From 7f94d170400a50c42affeae37713b8f7c5f3ffa2 Mon Sep 17 00:00:00 2001 From: Linzp Date: Thu, 4 Jun 2026 18:12:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B3=95=E5=8A=9E=E4=B8=80=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + package.json | 5 +- src/components/Enum/commonStatus.js | 12 + src/components/Enum/confirm.js | 12 + src/components/Enum/degree.js | 26 + src/components/Enum/gender.js | 12 + src/components/Enum/index.js | 77 +- src/components/Enum/locale/en-US.js | 33 + src/components/Enum/locale/zh-CN.js | 33 + src/components/Enum/marital.js | 12 + src/components/Enum/openStatus.js | 12 + src/components/Enum/phoneState.js | 20 + src/components/Enum/political.js | 16 + src/components/Enum/withLocale.js | 22 + .../fields/CheckboxFilterItem.js | 0 .../AdvancedFilter/fields/CityFilterItem.js | 113 - .../AdvancedFilter/fields/InputFilterItem.js | 52 - .../AdvancedFilter/fields/ListFilterItem.js | 81 - .../AdvancedFilter/fields/SelectFilterItem.js | 0 .../Filter/AdvancedFilter/fields/index.js | 7 - src/components/Filter/AdvancedFilter/index.js | 87 - src/components/Filter/Filter.js | 27 - src/components/Filter/FilterItem.js | 28 - src/components/Filter/FilterItemContainer.js | 5 - src/components/Filter/FilterLines.js | 156 - src/components/Filter/FilterOuter.js | 14 - src/components/Filter/FilterProvider.js | 41 - src/components/Filter/FilterValueDisplay.js | 78 - src/components/Filter/PopoverItem.js | 86 - src/components/Filter/README.md | 3052 +++++++---------- src/components/Filter/SearchInput.js | 19 - src/components/Filter/context.js | 7 - .../Filter/createFilterValueMapper.js | 86 - src/components/Filter/doc/example.json | 172 +- .../Filter/fields/DatePickerFilterItem.js | 21 - .../fields/DateRangePickerFilterItem.js | 29 - .../Filter/fields/InputFilterItem.js | 43 - .../Filter/fields/NumberRangeFilterItem.js | 90 - .../fields/TypeDateRangePickerFilterItem.js | 57 - src/components/Filter/fields/index.js | 49 - src/components/Filter/filterInterceptors.js | 67 - src/components/Filter/filterToUrlParams.js | 192 -- src/components/Filter/getFilterValue.js | 15 - src/components/Filter/index.js | 61 +- src/components/Filter/locale/en-US.js | 21 - src/components/Filter/locale/index.js | 8 - src/components/Filter/locale/zh-CN.js | 21 - src/components/Filter/pickSelectValues.js | 39 - src/components/Filter/style.module.scss | 468 --- src/components/Filter/useUrlFilter.js | 93 - src/components/Filter/useUrlFilterValue.js | 77 - src/components/Filter/withFieldItem.js | 24 - src/components/Filter/withFilterValue.js | 27 - src/components/HistoryStore/index.js | 11 +- src/components/HistoryStore/locale/en-US.js | 5 + src/components/HistoryStore/locale/zh-CN.js | 5 + src/components/HistoryStore/withLocale.js | 11 + 57 files changed, 1456 insertions(+), 4382 deletions(-) create mode 100644 src/components/Enum/commonStatus.js create mode 100644 src/components/Enum/confirm.js create mode 100644 src/components/Enum/degree.js create mode 100644 src/components/Enum/gender.js create mode 100644 src/components/Enum/locale/en-US.js create mode 100644 src/components/Enum/locale/zh-CN.js create mode 100644 src/components/Enum/marital.js create mode 100644 src/components/Enum/openStatus.js create mode 100644 src/components/Enum/phoneState.js create mode 100644 src/components/Enum/political.js create mode 100644 src/components/Enum/withLocale.js delete mode 100644 src/components/Filter/AdvancedFilter/fields/CheckboxFilterItem.js delete mode 100644 src/components/Filter/AdvancedFilter/fields/CityFilterItem.js delete mode 100644 src/components/Filter/AdvancedFilter/fields/InputFilterItem.js delete mode 100644 src/components/Filter/AdvancedFilter/fields/ListFilterItem.js delete mode 100644 src/components/Filter/AdvancedFilter/fields/SelectFilterItem.js delete mode 100644 src/components/Filter/AdvancedFilter/fields/index.js delete mode 100644 src/components/Filter/AdvancedFilter/index.js delete mode 100644 src/components/Filter/Filter.js delete mode 100644 src/components/Filter/FilterItem.js delete mode 100644 src/components/Filter/FilterItemContainer.js delete mode 100644 src/components/Filter/FilterLines.js delete mode 100644 src/components/Filter/FilterOuter.js delete mode 100644 src/components/Filter/FilterProvider.js delete mode 100644 src/components/Filter/FilterValueDisplay.js delete mode 100644 src/components/Filter/PopoverItem.js delete mode 100644 src/components/Filter/SearchInput.js delete mode 100644 src/components/Filter/context.js delete mode 100644 src/components/Filter/createFilterValueMapper.js delete mode 100644 src/components/Filter/fields/DatePickerFilterItem.js delete mode 100644 src/components/Filter/fields/DateRangePickerFilterItem.js delete mode 100644 src/components/Filter/fields/InputFilterItem.js delete mode 100644 src/components/Filter/fields/NumberRangeFilterItem.js delete mode 100644 src/components/Filter/fields/TypeDateRangePickerFilterItem.js delete mode 100644 src/components/Filter/fields/index.js delete mode 100644 src/components/Filter/filterInterceptors.js delete mode 100644 src/components/Filter/filterToUrlParams.js delete mode 100644 src/components/Filter/getFilterValue.js delete mode 100644 src/components/Filter/locale/en-US.js delete mode 100644 src/components/Filter/locale/index.js delete mode 100644 src/components/Filter/locale/zh-CN.js delete mode 100644 src/components/Filter/pickSelectValues.js delete mode 100644 src/components/Filter/style.module.scss delete mode 100644 src/components/Filter/useUrlFilter.js delete mode 100644 src/components/Filter/useUrlFilterValue.js delete mode 100644 src/components/Filter/withFieldItem.js delete mode 100644 src/components/Filter/withFilterValue.js create mode 100644 src/components/HistoryStore/locale/en-US.js create mode 100644 src/components/HistoryStore/locale/zh-CN.js create mode 100644 src/components/HistoryStore/withLocale.js diff --git a/.gitignore b/.gitignore index da1b4aff..f7e98bb7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ yarn-error.log* package-lock.json .idea .codebuddy +prompts diff --git a/package.json b/package.json index 3d2a9774..cd30bc51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne-components/components-core", - "version": "0.4.75", + "version": "0.5.0", "files": [ "build" ], @@ -35,6 +35,7 @@ "@kne/react-fetch": "^1.5.5", "@kne/react-file": "^0.1.35", "@kne/react-file-type": "^1.0.5", + "@kne/react-filter": "^1.0.5", "@kne/react-form-antd": "^4.1.0", "@kne/react-form-plus": "^0.1.5", "@kne/react-icon": "^0.1.3", @@ -102,7 +103,7 @@ "devDependencies": { "@kne/craco": "^7.1.3", "@kne/md-doc": "^0.1.16", - "@kne/modules-dev": "^2.3.0", + "@kne/modules-dev": "^2.3.4", "@kne/react-error-boundary": "^0.1.1", "antd": "6.0.0", "http-proxy-middleware": "^2.0.6", diff --git a/src/components/Enum/commonStatus.js b/src/components/Enum/commonStatus.js new file mode 100644 index 00000000..1471d844 --- /dev/null +++ b/src/components/Enum/commonStatus.js @@ -0,0 +1,12 @@ +import { createFormatMessage } from './withLocale'; + +const commonStatus = ({ locale }) => { + const formatMessage = createFormatMessage(locale); + return [{ + value: 'open', description: formatMessage({ id: 'CommonStatusOpen' }), type: 'success', + }, { + value: 'close', description: formatMessage({ id: 'CommonStatusClose' }), type: 'danger' + }]; +}; + +export default commonStatus; diff --git a/src/components/Enum/confirm.js b/src/components/Enum/confirm.js new file mode 100644 index 00000000..d12ec954 --- /dev/null +++ b/src/components/Enum/confirm.js @@ -0,0 +1,12 @@ +import { createFormatMessage } from './withLocale'; + +const confirm = ({ locale }) => { + const formatMessage = createFormatMessage(locale); + return [{ + description: formatMessage({ id: 'ConfirmYes' }), value: "Y", + }, { + description: formatMessage({ id: 'ConfirmNo' }), value: "N", + }]; +}; + +export default confirm; diff --git a/src/components/Enum/degree.js b/src/components/Enum/degree.js new file mode 100644 index 00000000..b289f5e9 --- /dev/null +++ b/src/components/Enum/degree.js @@ -0,0 +1,26 @@ +import { createFormatMessage } from './withLocale'; + +const degree = ({ locale }) => { + const formatMessage = createFormatMessage(locale); + return [{ + description: formatMessage({ id: 'DegreeJuniorHigh' }), value: 10, + }, { + description: formatMessage({ id: 'DegreeSecondaryVocational' }), value: 20, + }, { + description: formatMessage({ id: 'DegreeSeniorHigh' }), value: 30, + }, { + description: formatMessage({ id: 'DegreeJuniorCollege' }), value: 40, + }, { + description: formatMessage({ id: 'DegreeBachelor' }), value: 50, + }, { + description: formatMessage({ id: 'DegreeMaster' }), value: 60, + }, { + description: formatMessage({ id: 'DegreeDoctor' }), value: 70, + }, { + description: formatMessage({ id: 'DegreePostDoc' }), value: 75, + }, { + description: formatMessage({ id: 'DegreeUnlimited' }), value: 999, + }]; +}; + +export default degree; diff --git a/src/components/Enum/gender.js b/src/components/Enum/gender.js new file mode 100644 index 00000000..30bc6fd6 --- /dev/null +++ b/src/components/Enum/gender.js @@ -0,0 +1,12 @@ +import { createFormatMessage } from './withLocale'; + +const gender = ({ locale }) => { + const formatMessage = createFormatMessage(locale); + return [{ + value: "M", description: formatMessage({ id: 'GenderMale' }), + }, { + value: "F", description: formatMessage({ id: 'GenderFemale' }), + }]; +}; + +export default gender; diff --git a/src/components/Enum/index.js b/src/components/Enum/index.js index bf6e8b5a..8bf07af0 100644 --- a/src/components/Enum/index.js +++ b/src/components/Enum/index.js @@ -1,64 +1,27 @@ import {preset} from "@kne/react-enum"; -import transform from "lodash/transform"; -const degree = [{ - description: "初中", value: 10, -}, { - description: "中专", value: 20, -}, { - description: "高中", value: 30, -}, { - description: "大专", value: 40, -}, { - description: "本科", value: 50, -}, { - description: "硕士研究生", value: 60, -}, { - description: "博士研究生", value: 70, -}, { - description: "博士后", value: 75, -}, { - description: "学历不限", value: 999, -}]; - -const phoneState = [{ - value: 0, description: "空号", -}, { - value: 1, description: "实号", -}, { - value: 2, description: "停机", -}, { - value: 3, description: "库无", -}, { - value: 4, description: "沉默号", -}, { - value: 5, description: "风险号", -}]; - -const openStatus = [{value: 'open', description: '开启', type: 'success',}, { - value: 'closed', description: '关闭', type: 'danger' -}]; - -const baseLoaders = [['openStatus', openStatus], ['commonStatus', () => [{ - value: 'open', description: '开启', type: 'success', -}, { - value: 'close', description: '关闭', type: 'danger' -}]], ["gender", () => [{value: "M", description: "男"}, { - value: "F", description: "女", -},],], ["marital", () => [{description: "已婚", value: "Y"}, { - description: "未婚", value: "N", -},],], ["confirm", () => [{description: "是", value: "Y"}, { - description: "否", value: "N", -},],], ["political", () => [{description: "中共党员", value: "中共党员"}, { - description: "共青团员", value: "共青团员", -}, {description: "群众", value: "群众"}, { - description: "其他党派", value: "其他党派", -},],], ["phoneStateEnum", phoneState], ["phoneState", phoneState], ["degreeEnum", degree], ["degree", degree]]; +import degree from './degree'; +import phoneState from './phoneState'; +import openStatus from './openStatus'; +import commonStatus from './commonStatus'; +import gender from './gender'; +import marital from './marital'; +import confirm from './confirm'; +import political from './political'; preset({ - base: transform(baseLoaders, (result, value) => { - result[value[0]] = value[1]; - }, {}), + base: { + openStatus, + commonStatus, + gender, + marital, + confirm, + political, + phoneStateEnum: phoneState, + phoneState, + degreeEnum: degree, + degree + }, }); export {default} from "@kne/react-enum"; diff --git a/src/components/Enum/locale/en-US.js b/src/components/Enum/locale/en-US.js new file mode 100644 index 00000000..0c74feb7 --- /dev/null +++ b/src/components/Enum/locale/en-US.js @@ -0,0 +1,33 @@ +const message = { + DegreeJuniorHigh: "Junior High School", + DegreeSecondaryVocational: "Secondary Vocational", + DegreeSeniorHigh: "Senior High School", + DegreeJuniorCollege: "Junior College", + DegreeBachelor: "Bachelor", + DegreeMaster: "Master", + DegreeDoctor: "Doctor", + DegreePostDoc: "Postdoctoral", + DegreeUnlimited: "No Limit", + PhoneStateEmpty: "Empty", + PhoneStateValid: "Valid", + PhoneStateSuspended: "Suspended", + PhoneStateNotFound: "Not Found", + PhoneStateSilent: "Silent", + PhoneStateRisk: "Risk", + OpenStatusOpen: "Open", + OpenStatusClosed: "Closed", + CommonStatusOpen: "Open", + CommonStatusClose: "Closed", + GenderMale: "Male", + GenderFemale: "Female", + MaritalMarried: "Married", + MaritalSingle: "Single", + ConfirmYes: "Yes", + ConfirmNo: "No", + PoliticalPartyMember: "CPC Member", + PoliticalLeagueMember: "CYLC Member", + PoliticalMasses: "Masses", + PoliticalOther: "Other Parties" +}; + +export default message; diff --git a/src/components/Enum/locale/zh-CN.js b/src/components/Enum/locale/zh-CN.js new file mode 100644 index 00000000..2ac93392 --- /dev/null +++ b/src/components/Enum/locale/zh-CN.js @@ -0,0 +1,33 @@ +const message = { + DegreeJuniorHigh: "初中", + DegreeSecondaryVocational: "中专", + DegreeSeniorHigh: "高中", + DegreeJuniorCollege: "大专", + DegreeBachelor: "本科", + DegreeMaster: "硕士研究生", + DegreeDoctor: "博士研究生", + DegreePostDoc: "博士后", + DegreeUnlimited: "学历不限", + PhoneStateEmpty: "空号", + PhoneStateValid: "实号", + PhoneStateSuspended: "停机", + PhoneStateNotFound: "库无", + PhoneStateSilent: "沉默号", + PhoneStateRisk: "风险号", + OpenStatusOpen: "开启", + OpenStatusClosed: "关闭", + CommonStatusOpen: "开启", + CommonStatusClose: "关闭", + GenderMale: "男", + GenderFemale: "女", + MaritalMarried: "已婚", + MaritalSingle: "未婚", + ConfirmYes: "是", + ConfirmNo: "否", + PoliticalPartyMember: "中共党员", + PoliticalLeagueMember: "共青团员", + PoliticalMasses: "群众", + PoliticalOther: "其他党派" +}; + +export default message; diff --git a/src/components/Enum/marital.js b/src/components/Enum/marital.js new file mode 100644 index 00000000..dfbfedca --- /dev/null +++ b/src/components/Enum/marital.js @@ -0,0 +1,12 @@ +import { createFormatMessage } from './withLocale'; + +const marital = ({ locale }) => { + const formatMessage = createFormatMessage(locale); + return [{ + description: formatMessage({ id: 'MaritalMarried' }), value: "Y", + }, { + description: formatMessage({ id: 'MaritalSingle' }), value: "N", + }]; +}; + +export default marital; diff --git a/src/components/Enum/openStatus.js b/src/components/Enum/openStatus.js new file mode 100644 index 00000000..5b09fcd2 --- /dev/null +++ b/src/components/Enum/openStatus.js @@ -0,0 +1,12 @@ +import { createFormatMessage } from './withLocale'; + +const openStatus = ({ locale }) => { + const formatMessage = createFormatMessage(locale); + return [{ + value: 'open', description: formatMessage({ id: 'OpenStatusOpen' }), type: 'success', + }, { + value: 'closed', description: formatMessage({ id: 'OpenStatusClosed' }), type: 'danger' + }]; +}; + +export default openStatus; diff --git a/src/components/Enum/phoneState.js b/src/components/Enum/phoneState.js new file mode 100644 index 00000000..c1d4b2a2 --- /dev/null +++ b/src/components/Enum/phoneState.js @@ -0,0 +1,20 @@ +import { createFormatMessage } from './withLocale'; + +const phoneState = ({ locale }) => { + const formatMessage = createFormatMessage(locale); + return [{ + value: 0, description: formatMessage({ id: 'PhoneStateEmpty' }), + }, { + value: 1, description: formatMessage({ id: 'PhoneStateValid' }), + }, { + value: 2, description: formatMessage({ id: 'PhoneStateSuspended' }), + }, { + value: 3, description: formatMessage({ id: 'PhoneStateNotFound' }), + }, { + value: 4, description: formatMessage({ id: 'PhoneStateSilent' }), + }, { + value: 5, description: formatMessage({ id: 'PhoneStateRisk' }), + }]; +}; + +export default phoneState; diff --git a/src/components/Enum/political.js b/src/components/Enum/political.js new file mode 100644 index 00000000..1ceff728 --- /dev/null +++ b/src/components/Enum/political.js @@ -0,0 +1,16 @@ +import { createFormatMessage } from './withLocale'; + +const political = ({ locale }) => { + const formatMessage = createFormatMessage(locale); + return [{ + description: formatMessage({ id: 'PoliticalPartyMember' }), value: "中共党员", + }, { + description: formatMessage({ id: 'PoliticalLeagueMember' }), value: "共青团员", + }, { + description: formatMessage({ id: 'PoliticalMasses' }), value: "群众", + }, { + description: formatMessage({ id: 'PoliticalOther' }), value: "其他党派", + }]; +}; + +export default political; diff --git a/src/components/Enum/withLocale.js b/src/components/Enum/withLocale.js new file mode 100644 index 00000000..386b6a14 --- /dev/null +++ b/src/components/Enum/withLocale.js @@ -0,0 +1,22 @@ +import {createWithIntlProvider, createIntl} from '@kne/react-intl'; +import zhCN from './locale/zh-CN'; +import enUS from './locale/en-US'; + +const withLocale = createWithIntlProvider({ + defaultLocale: 'zh-CN', messages: { + 'zh-CN': zhCN, 'en-US': enUS + }, namespace: 'Enum' +}); + +export const createFormatMessage = locale => { + const {formatMessage} = createIntl({ + locale, + messages: { + 'zh-CN': zhCN, 'en-US': enUS + }, + namespace: 'Enum' + }); + return formatMessage; +}; + +export default withLocale; diff --git a/src/components/Filter/AdvancedFilter/fields/CheckboxFilterItem.js b/src/components/Filter/AdvancedFilter/fields/CheckboxFilterItem.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/Filter/AdvancedFilter/fields/CityFilterItem.js b/src/components/Filter/AdvancedFilter/fields/CityFilterItem.js deleted file mode 100644 index 450c48bf..00000000 --- a/src/components/Filter/AdvancedFilter/fields/CityFilterItem.js +++ /dev/null @@ -1,113 +0,0 @@ -import {App, Tag} from "antd"; -import AddressSelectField, { - getLabelForLocal, - withAddressApi, -} from "@common/components/AddressSelectField"; -import {FormattedMessage, useIntl} from "@components/Intl"; -import {usePreset} from "@components/Global"; -import {useMemo} from "react"; -import style from "../../style.module.scss"; - -const {CheckableTag} = Tag; - -const CityFilterItemInner = ({ - value, - onChange, - single = false, - maxLength = 5, - addressApi, - ...props - }) => { - const {message} = App.useApp(); - const {locale} = usePreset(); - const {formatMessage} = useIntl({moduleName: "Filter"}); - const cityList = useMemo(() => { - return addressApi.getChinaHotCities(); - }, [addressApi]); - return ( - <> - {cityList.map((city) => { - const {code} = city; - const name = getLabelForLocal(city, locale); - return ( - value === code) - } - onChange={(checked) => { - if (single) { - onChange(checked ? {value: code, label: name} : null); - return; - } - const newValue = (value || []).slice(0); - checked - ? newValue.push({value: code, label: name}) - : (() => { - const index = newValue.findIndex( - ({value}) => value === code - ); - newValue.splice(index, 1); - })(); - if (newValue.length > maxLength) { - message.error( - formatMessage( - {id: "maxSelectedCount"}, - {count: maxLength} - ) - ); - return; - } - onChange(newValue); - }} - > - {name} - - ); - })} - 0) && - !cityList.find(({code}) => - single - ? value?.value === code - : !!(value || []).find(({value}) => value === code) - ) - } - > - - value) - } - single={single} - onChange={(value, ...args) => { - const getCityValue = (value) => { - const {city} = addressApi.getCity(value); - return {value: city?.code, label: city?.name}; - }; - onChange( - single - ? getCityValue(value) - : value.map((value) => getCityValue(value)) - ); - }} - /> - - - ); -}; -const CityFilterItem = withAddressApi(CityFilterItemInner); -export default CityFilterItem; diff --git a/src/components/Filter/AdvancedFilter/fields/InputFilterItem.js b/src/components/Filter/AdvancedFilter/fields/InputFilterItem.js deleted file mode 100644 index 003684f5..00000000 --- a/src/components/Filter/AdvancedFilter/fields/InputFilterItem.js +++ /dev/null @@ -1,52 +0,0 @@ -import { Input, Space, Button } from "antd"; -import { useState, useEffect, useRef } from "react"; -import useSimulationBlur from "@kne/use-simulation-blur"; -import { useIntl } from "@components/Intl"; - -const InputFilterItem = ({ value, label, onChange, ...props }) => { - const propsValue = value?.value; - const [inputValue, setInputValue] = useState(propsValue || ""); - const [active, setActive] = useState(false); - const { formatMessage } = useIntl({ moduleName: "Filter" }); - const searchHandler = () => { - onChange(inputValue ? { label: inputValue, value: inputValue } : null); - }; - const ref = useSimulationBlur(() => { - setActive(false); - searchHandler(); - }); - const inputValueRef = useRef(""); - inputValueRef.current = inputValue; - - useEffect(() => { - if (propsValue !== inputValueRef.current) { - setInputValue(propsValue); - } - }, [propsValue]); - return ( - - - { - setActive(true); - }} - onChange={(e) => { - setInputValue(e.target.value); - }} - onPressEnter={searchHandler} - /> - {active && ( - - )} - - - ); -}; - -export default InputFilterItem; diff --git a/src/components/Filter/AdvancedFilter/fields/ListFilterItem.js b/src/components/Filter/AdvancedFilter/fields/ListFilterItem.js deleted file mode 100644 index f7f48ef8..00000000 --- a/src/components/Filter/AdvancedFilter/fields/ListFilterItem.js +++ /dev/null @@ -1,81 +0,0 @@ -import {Tag, App} from "antd"; -import isEqual from "lodash/isEqual"; -import * as fields from "../../fields"; -import {useIntl} from "@components/Intl"; - -const {CheckableTag} = Tag; -const ListFilterItem = ({ - value, - onChange, - label, - single = false, - maxLength = 5, - items = [], - custom, - }) => { - const {message} = App.useApp(); - const {formatMessage} = useIntl({moduleName: "Filter"}); - return ( - <> - {items.map(({label, value: itemValue}) => { - return ( - isEqual(itemValue, value)) - } - onChange={(checked) => { - if (single) { - onChange(checked ? {value: itemValue, label} : null); - return; - } - const newValue = (value || []).slice(0); - checked - ? newValue.push({value: itemValue, label}) - : (() => { - const index = newValue.find(({value}) => - isEqual(itemValue, value) - ); - newValue.splice(index, 1); - })(); - if (newValue.length > maxLength) { - message.error( - formatMessage( - {id: "maxSelectedCount"}, - {count: maxLength} - ) - ); - return; - } - onChange(newValue); - }} - > - {label} - - ); - })} - {custom && - (() => { - const ComponentItem = Object.values(fields).find( - (item) => item === custom.type - ); - if (!ComponentItem) { - return null; - } - return ( - - ); - })()} - - ); -}; - -export default ListFilterItem; diff --git a/src/components/Filter/AdvancedFilter/fields/SelectFilterItem.js b/src/components/Filter/AdvancedFilter/fields/SelectFilterItem.js deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/Filter/AdvancedFilter/fields/index.js b/src/components/Filter/AdvancedFilter/fields/index.js deleted file mode 100644 index 38cb5177..00000000 --- a/src/components/Filter/AdvancedFilter/fields/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import CityFilterItem from "./CityFilterItem"; -import ListFilterItem from "./ListFilterItem"; -import InputFilterItem from "./InputFilterItem"; - -const fields = { CityFilterItem, ListFilterItem, InputFilterItem }; - -export default fields; diff --git a/src/components/Filter/AdvancedFilter/index.js b/src/components/Filter/AdvancedFilter/index.js deleted file mode 100644 index 98a82250..00000000 --- a/src/components/Filter/AdvancedFilter/index.js +++ /dev/null @@ -1,87 +0,0 @@ -import FilterOuter from "../FilterOuter"; -import MoreFilterLines from "../FilterLines"; -import { Flex } from "antd"; -import advancedFields from "./fields"; -import { createWithIntl, useIntl } from "@components/Intl"; -import get from "lodash/get"; -import { useContext } from "../context"; -import style from "../style.module.scss"; -import FilterValueDisplay from "../FilterValueDisplay"; -import importMessages from "../locale"; - -const Line = ({ list }) => { - const { value, onChange } = useContext(); - return ( - - {list.map((item, index) => { - const ComponentItem = item.type; - return ( - -
{item.props.label}:
- - - onChange({ - name: item.props.name, - label: item.props.label, - value, - }) - : item.props.onChange - } - /> - -
- ); - })} -
- ); -}; - -const AdvancedFilter = createWithIntl({ importMessages, moduleName: "Filter" })( - (props) => { - const { formatMessage } = useIntl({ moduleName: "Filter" }); - return ( - - {({ value, onChange, props }) => { - const { list, more } = props; - return ( -
- - {list.map((item, index) => { - return ; - })} - {more && ( - - )} - - {value && value.length > 0 && ( - - )} -
- ); - }} -
- ); - } -); - -AdvancedFilter.fields = advancedFields; -export default AdvancedFilter; - -export { advancedFields }; diff --git a/src/components/Filter/Filter.js b/src/components/Filter/Filter.js deleted file mode 100644 index 99b66fb9..00000000 --- a/src/components/Filter/Filter.js +++ /dev/null @@ -1,27 +0,0 @@ -import FilterLines from "./FilterLines"; -import FilterValueDisplay from "./FilterValueDisplay"; -import FilterOuter from "./FilterOuter"; - -const Filter = ({defaultValue=[],...props}) => { - return ( - - {({ props, value, onChange }) => { - const { extraExpand, ...others } = props; - return ( - <> - - {value && value.length > 0 && ( - - )} - - ); - }} - - ); -}; - -export default Filter; diff --git a/src/components/Filter/FilterItem.js b/src/components/Filter/FilterItem.js deleted file mode 100644 index 22aec609..00000000 --- a/src/components/Filter/FilterItem.js +++ /dev/null @@ -1,28 +0,0 @@ -import classnames from "classnames"; -import { Space } from "antd"; -import Icon from "@components/Icon"; -import style from "./style.module.scss"; - -const FilterItem = ({ open, active, label, children }) => { - return ( - -
- -
{label}
- -
-
{children}
-
-
- ); -}; - -export default FilterItem; diff --git a/src/components/Filter/FilterItemContainer.js b/src/components/Filter/FilterItemContainer.js deleted file mode 100644 index df1d1ecd..00000000 --- a/src/components/Filter/FilterItemContainer.js +++ /dev/null @@ -1,5 +0,0 @@ -const FilterItemContainer = ({ children, ...props }) => { - return children(props); -}; - -export default FilterItemContainer; diff --git a/src/components/Filter/FilterLines.js b/src/components/Filter/FilterLines.js deleted file mode 100644 index 2357c75b..00000000 --- a/src/components/Filter/FilterLines.js +++ /dev/null @@ -1,156 +0,0 @@ -import classnames from "classnames"; -import {useState} from "react"; -import Icon from "@components/Icon"; -import style from "./style.module.scss"; -import {Col, Row, Space} from "antd"; -import {FormattedMessage, IntlProvider} from "@components/Intl"; -import importMessages from "./locale"; -import {useContext} from "./context"; -import get from "lodash/get"; - -const Line = ({list, children}) => { - const {value, onChange} = useContext(); - return (
- {list.filter((item) => !!item.type).map((item, index) => { - if (typeof item === "function") { - return item((props) => { - return { - index, - value: value ? get(value.get(props?.name), "value") : props?.value, - onChange: onChange ? (value) => onChange({ - name: props?.name, label: props?.label, value, - }) : props?.onChange, - }; - }); - } - const ComponentItem = item.type; - return ( onChange({ - name: item.props.name, label: item.props.label, value, - }) : item.props.onChange, - })} - key={item.key || item.props.name || index} - />); - })} - {children} -
); -}; - -const FilterLines = ({ - className, list = [], displayLine = 1, label, extra, children, - }) => { - const hasMore = list.length > displayLine; - const [isExpand, setIsExpand] = useState(false); - return ( - - - {list && list.length > 0 && (label || )} - - - - {list.slice(0, displayLine).map((item, index) => ( - {hasMore && isExpand === false && index === displayLine - 1 ? ( { - setIsExpand((value) => !value); - }} - > - - - ) : null} - ))} - - {extra} - - - - {children} - - - - - -
- {list.slice(displayLine).map((item, index) => ( - {index === list.length - displayLine - 1 && (<> - - - - - - - { - setIsExpand((value) => !value); - }} - > - - - - - - )} - ))} -
-
-
); -}; - -export default FilterLines; diff --git a/src/components/Filter/FilterOuter.js b/src/components/Filter/FilterOuter.js deleted file mode 100644 index b2197ab4..00000000 --- a/src/components/Filter/FilterOuter.js +++ /dev/null @@ -1,14 +0,0 @@ -import classnames from "classnames"; -import style from "./style.module.scss"; -import FilterProvider from './FilterProvider'; - -const FilterOuter = ({children, className, ...props}) => { - return - {(context) =>
- {children(context)} -
} -
- -}; - -export default FilterOuter; diff --git a/src/components/Filter/FilterProvider.js b/src/components/Filter/FilterProvider.js deleted file mode 100644 index bbf488d1..00000000 --- a/src/components/Filter/FilterProvider.js +++ /dev/null @@ -1,41 +0,0 @@ -import {Provider} from "./context"; -import useControlValue from "@kne/use-control-value"; -import clone from "lodash/clone"; -import {useMemo} from "react"; -import {isNotEmpty} from "@components/Common"; -import importMessages from "./locale"; - -import {IntlProvider} from "@components/Intl"; - -const FilterOuter = ({children, className, defaultValue = [], ...props}) => { - const [valueBase, onChange] = useControlValue(props); - - const value = useMemo(() => { - return valueBase.filter((item) => isNotEmpty(item.value)); - }, [valueBase]); - - const filterValue = useMemo(() => { - return new Map(value.map((item) => [item.name, item])); - }, [value]); - - return ( - - { - const newFilterValue = clone(filterValue); - item.value - ? newFilterValue.set(item.name, item) - : newFilterValue.delete(item.name); - onChange?.(Array.from(newFilterValue.values())); - }, - }} - > - {typeof children === 'function' ? children({props, value, onChange}) : children} - - - ); -}; - -export default FilterOuter; diff --git a/src/components/Filter/FilterValueDisplay.js b/src/components/Filter/FilterValueDisplay.js deleted file mode 100644 index f3d923b5..00000000 --- a/src/components/Filter/FilterValueDisplay.js +++ /dev/null @@ -1,78 +0,0 @@ -import { Button, Space } from "antd"; -import StateTag from "@components/StateTag"; -import { FormattedMessage, IntlProvider } from "@components/Intl"; -import style from "@components/Filter/style.module.scss"; -import classnames from "classnames"; -import importMessages from "@components/Filter/locale"; - -const FilterValueDisplay = ({ value: filterValue, extraExpand, onChange }) => { - return ( - - - - - - {/*
*/} -
- {filterValue.map(({ name, label, value }, index) => { - return ( - { - return item.label; - }) - .join(",") - : value.label - } - closable - onClose={() => { - const newValue = filterValue.slice(0); - newValue.splice(index, 1); - onChange(newValue); - }} - /> - ); - })} - - {extraExpand} - - - - {extraExpand} - - - -
- {/*
*/} -
-
- ); -}; - -export default FilterValueDisplay; diff --git a/src/components/Filter/PopoverItem.js b/src/components/Filter/PopoverItem.js deleted file mode 100644 index 49f4e15d..00000000 --- a/src/components/Filter/PopoverItem.js +++ /dev/null @@ -1,86 +0,0 @@ -import {useState, useMemo} from "react"; -import {Button, Col, Popover, Row} from "antd"; -import FilterItem from "./FilterItem"; -import isNotEmpty from "@common/utils/isNotEmpty"; -import classnames from "classnames"; -import {FormattedMessage} from "@components/Intl"; -import style from "./style.module.scss"; - -const PopoverItem = ({ - value, - label, - onValidate, - overlayClassName, - placement = 'bottomLeft', - onOpenChange, - onChange, - children, - }) => { - const [state, setState] = useState(value); - const [open, setOpen] = useState(false); - const disabled = useMemo(() => { - return onValidate && !onValidate(state); - }, [onValidate, state]); - return ( { - setOpen(open); - setState(value); - onOpenChange && onOpenChange(open); - }} - content={ { - e.stopPropagation(); - }} - > -
- {children({value: state, onChange: setState})} -
- - - - - - - - -
} - > - - - -
); -}; - -export default PopoverItem; diff --git a/src/components/Filter/README.md b/src/components/Filter/README.md index 4b350a47..84e069b1 100644 --- a/src/components/Filter/README.md +++ b/src/components/Filter/README.md @@ -1,407 +1,219 @@ -# Filter +# react-filter -### 概述 +### 描述 -Filter 是一个功能强大的筛选组件库,用于构建灵活的筛选条件界面。该组件提供了多种预置的筛选字段类型,支持自定义筛选项,并提供了完整的筛选值管理和展示功能。 +A React filter component library with multiple filter types, flexible layouts, and URL parameter synchronization. -核心特性包括:丰富的预置筛选字段,涵盖文本输入、日期选择、城市选择、用户选择、行业选择、职能选择等多种类型;灵活的筛选值管理,支持受控和非受控模式;声明式筛选值映射(`createFilterValueMapper`),统一处理多选、单选、自定义转换等值提取逻辑;URL 参数序列化与初始化(`filterToUrlParams` / `createUrlFilterReader` / `useUrlFilter`),筛选值以 `filterParams[key]` 格式序列化到 URL(前缀可自定义),从 URL 参数构建初始筛选状态并自动清理已消费参数;值格式拦截器(`filterInterceptors`),自动处理 SuperSelect 组件 `{ id, name }` 与 Filter 上下文 `{ label, value }` 格式之间的转换;支持展开/收起筛选项,避免筛选条件过多导致界面混乱;提供高级筛选组件,适用于复杂筛选场景;支持自定义字段和组合使用,满足各种业务需求;内置搜索输入框和筛选值展示组件,提升用户体验。 +### 安装 -适用于数据列表、表格筛选、报表查询等需要多条件筛选的场景。组件采用 Context API 进行状态管理,支持嵌套使用和组合,能够满足企业级应用中各种复杂的筛选需求。 +```shell +npm i --save @kne/react-filter +``` -### 示例(全屏) +### 概述 -#### 示例代码 +### React Filter -- 基础用法 -- 展示 Filter 组件的基本使用方式,包括各种常见的筛选字段类型 -- _Filter(@components/Filter) +一个功能强大的 React 筛选组件库,支持多种筛选字段类型、灵活的布局方式,以及完善的 URL 参数双向同步能力。 -```jsx -const { - default: Filter, - InputFilterItem, - DatePickerFilterItem, - DateRangePickerFilterItem, - TypeDateRangePickerFilterItem, - CityFilterItem, - AdvancedSelectFilterItem, - SuperSelectFilterItem, - SuperSelectUserFilterItem, - UserFilterItem, - FunctionSelectFilterItem, - IndustrySelectFilterItem, - getFilterValue, - FilterItemContainer, -} = _Filter; -const { useState } = React; -const BaseExample = () => { - const [value, onChange] = useState([]); - return ( - { - console.log(getFilterValue(value)); - onChange(value); - }} - extra={} - list={[ - [ - , - , - - {(props) => ( -
- { - return { - pageData: [ - { label: "第一项", value: 1 }, - { - label: "第二项", - value: 2, - disabled: true, - }, - { - label: "第三项", - value: 3, - }, - ], - }; - }, - }} - /> -
- )} -
, - , - , - , - , - { - return { - pageData: [ - { - label: "用户一", - value: 1, - description: "我是用户描述", - }, - { - label: "用户二", - value: 2, - description: "我是用户描述", - }, - { - label: "用户三", - value: 3, - description: "我是用户描述", - }, - ], - }; - }, - }} - />, - , - , - , - , - , - , - ], - ]} - /> - ); -}; +### 主要特性 -render(); +- **多种筛选字段类型**:支持输入框、数字区间、日期选择、日期范围、下拉选择等常用筛选类型,以及职能、行业、城市等业务选择器 +- **灵活布局**:支持普通筛选(横向布局)和高级筛选(垂直布局)两种模式 +- **展开收起**:筛选行支持展开收起功能,优化页面空间利用 +- **已选值展示**:自动展示已选筛选条件,支持单独删除和清空全部 +- **弹出层交互**:支持弹出层形式的筛选交互,确认后才生效 +- **URL 参数同步**:支持筛选值与 URL 参数的双向序列化/反序列化,自动清除已消费参数 +- **数据格式拦截器**:内置 `{id, name}` ↔ `{label, value}` 格式转换拦截器,适配 SuperSelect 场景 +- **声明式值映射**:提供 `createFilterValueMapper` 按字段声明转换规则,简化 `getFilterValue` 结果处理 +- **国际化支持**:内置中英文语言包,支持多语言切换 +- **高阶组件**:提供 `withFilterValue` 和 `withFieldItem` 高阶组件,便于扩展自定义字段 -``` +### 适用场景 -- 展示和Enum一起使用 -- -- _Filter(@components/Filter),_Enum(@components/Enum) +- 数据列表页面的筛选功能 +- 复杂表单的筛选条件配置 +- 多条件组合查询场景 +- 需要展示已选筛选条件的场景 +- 需要筛选状态与 URL 参数同步保持的页面 -```jsx -const { - default: Filter, SuperSelectFilterItem, getFilterValue -} = _Filter; -const {default: Enum} = _Enum; -const {useState} = React; -const BaseExample = () => { - const [value, onChange] = useState([]); - return ( { - console.log(getFilterValue(value)); - onChange(value); - }} - list={[[ { - return {(options) => children({options})} - }}/>]]} - />); -}; +### 快速开始 -render(); +```javascript +import Filter, { fields } from '@kne/react-filter'; +import '@kne/react-filter/dist/index.css'; -``` +const { InputFilterItem, NumberRangeFilterItem, DatePickerFilterItem } = fields; -- 高级筛选 -- 展示 AdvancedFilter 组件的高级筛选功能,适用于复杂筛选场景 -- _Filter(@components/Filter) +function MyComponent() { + const [filterValue, setFilterValue] = useState([]); -```jsx -const { - default: Filter, - AdvancedFilter, - InputFilterItem, - DatePickerFilterItem, - DateRangePickerFilterItem, - TypeDateRangePickerFilterItem, - CityFilterItem, - AdvancedSelectFilterItem, - UserFilterItem, - FunctionSelectFilterItem, - IndustrySelectFilterItem, - NumberRangeFilterItem, - getFilterValue, - FilterItemContainer, -} = _Filter; -const { useState } = React; + const handleSearch = () => { + const params = Filter.getFilterValue(filterValue); + console.log('筛选参数:', params); + }; -const { - CityFilterItem: CityAdvancedFilterItem, - ListFilterItem, - InputFilterItem: InputAdvancedFilterItem, -} = AdvancedFilter.fields; -const BaseExample = () => { - const [value, onChange] = useState([]); return ( - { - console.log(getFilterValue(value)); - onChange(value); - }} + ], - [], [ - } - />, + { type: InputFilterItem, props: { name: 'keyword', label: '关键词' } }, + { type: NumberRangeFilterItem, props: { name: 'amount', label: '金额' } } ], - [], - ]} - more={[ - , - , - - {(props) => ( -
- { - return { - pageData: [ - { label: "第一项", value: 1 }, - { - label: "第二项", - value: 2, - disabled: true, - }, - { - label: "第三项", - value: 3, - }, - ], - }; - }, - }} - /> -
- )} -
, - , - , - , - { - return { - pageData: [ - { - label: "用户一", - value: 1, - description: "我是用户描述", - }, - { - label: "用户二", - value: 2, - description: "我是用户描述", - }, - { - label: "用户三", - value: 3, - description: "我是用户描述", - }, - ], - }; - }, - }} - />, - , - , + [ + { type: DatePickerFilterItem, props: { name: 'date', label: '日期' } } + ] ]} + displayLine={1} + extra={} /> ); -}; +} +``` -render(); +### 核心组件 + +| 组件 | 说明 | +|------|------| +| `Filter` | 主筛选组件,横向布局,支持展开收起 | +| `AdvancedFilter` | 高级筛选组件,垂直布局 | +| `FilterValueDisplay` | 已选值展示组件 | +| `PopoverItem` | 弹出层筛选项组件 | +| `FilterItem` | 筛选项容器组件 | +| `FilterLines` | 筛选行组件 | +| `FilterProvider` | 状态管理组件 | + +### 筛选字段 + +| 字段组件 | 说明 | +|----------|------| +| `InputFilterItem` | 输入框筛选 | +| `NumberRangeFilterItem` | 数字区间筛选 | +| `DatePickerFilterItem` | 日期选择筛选 | +| `DateRangePickerFilterItem` | 日期范围筛选 | +| `TypeDateRangePickerFilterItem` | 类型日期范围筛选(日/周/月切换) | +| `SuperSelectFilterItem` | 通用选择器筛选(单选/多选/搜索/全选) | +| `SelectFunctionFilterItem` | 职能筛选(多级数据、拼音搜索) | +| `SelectIndustryFilterItem` | 行业筛选(多级数据、拼音搜索) | +| `SelectAddressFilterItem` | 城市筛选(国内外城市搜索) | + +### URL 参数工具 + +| 工具 | 说明 | +|------|------| +| `useUrlFilter` | 从 URL 参数初始化筛选状态的 hook | +| `useUrlFilterValue` | 简化版 URL 筛选 hook,自动解析 filterParams[key] 格式 | +| `filterToUrlParams` | 将筛选值序列化为 URL 参数 | +| `parseFilterEntry` | 解析 URL 参数中的单个筛选值项 | +| `takeFilterEntry` | 从 URL 参数中读取筛选值项 | +| `createUrlFilterReader` | 创建 URL 筛选参数读取器 | +| `createUrlParamsReader` | 创建通用 URL 参数读取器 | +| `stripConsumedUrlParams` | 移除已消费的 URL 参数 | + +### 其他工具 + +| 工具 | 说明 | +|------|------| +| `pickSelectValues` | 从筛选值中提取原始值数组 | +| `createFilterValueMapper` | 声明式创建 mapFilterValue 函数 | +| `filterInterceptors` | `{single, multi}` 拦截器集合 | +| `singleSelectInterceptor` | 单选格式转换拦截器 | +| `multiSelectInterceptor` | 多选格式转换拦截器 | + + +### 示例 -``` +#### 示例代码 -- 树形筛选 -- 展示 TreeFilterItem 树形选择组件的使用 -- _Filter(@components/Filter),antd(antd),_data(@components/Filter/doc/mock/tree-data.json) +- 基础筛选 +- 使用 Filter 主组件,展示关键词、金额、日期、部门等多种筛选字段的组合使用 +- _ReactFilter(@kne/react-filter)[import * as _ReactFilter from "@kne/react-filter"],(@kne/react-filter/dist/index.css),antd(antd) ```jsx -const { default: Filter, TreeFilterItem } = _Filter; -const { default: treeData } = _data; +const { default: Filter, fields } = _ReactFilter; +const { + InputFilterItem, NumberRangeFilterItem, DatePickerFilterItem, + DateRangePickerFilterItem, TypeDateRangePickerFilterItem, + SuperSelectFilterItem, SelectFunctionFilterItem, + SelectIndustryFilterItem, SelectAddressFilterItem +} = fields; +const { Flex, Button, message } = antd; const { useState } = React; -const { Space } = antd; + +const departmentOptions = [ + { value: 'tech', label: '技术研发部' }, + { value: 'product', label: '产品设计部' }, + { value: 'operation', label: '运营管理部' }, + { value: 'hr', label: '人力资源部' }, + { value: 'finance', label: '财务部' }, + { value: 'marketing', label: '市场营销部' } +]; const BaseExample = () => { - const [filter, setFilter] = useState([]); - const [filter2, setFilter2] = useState([]); + const [filterValue, setFilterValue] = useState([]); + + const handleSearch = () => { + const params = Filter.getFilterValue(filterValue); + message.info(`搜索参数: ${JSON.stringify(params, null, 2)}`); + console.log('筛选参数:', params); + }; return ( - + { - return treeData.children; - }, - }} - />, + { + type: InputFilterItem, + props: { name: 'keyword', label: '关键词', placeholder: '请输入关键词搜索' } + }, + { + type: NumberRangeFilterItem, + props: { name: 'amount', label: '金额', unit: '元', min: 0, max: 999999 } + }, + { + type: DatePickerFilterItem, + props: { name: 'createTime', label: '创建时间', format: 'YYYY-MM-DD' } + }, + { + type: DateRangePickerFilterItem, + props: { name: 'dateRange', label: '日期范围', format: 'YYYY-MM-DD' } + }, + { + type: TypeDateRangePickerFilterItem, + props: { name: 'typeDateRange', label: '快捷日期' } + } ], - ]} - /> - { - return treeData.children; - }, - }} - />, - ], + { + type: SuperSelectFilterItem, + props: { name: 'department', label: '部门', options: departmentOptions } + }, + { + type: SelectFunctionFilterItem, + props: { name: 'function', label: '职能' } + }, + { + type: SelectIndustryFilterItem, + props: { name: 'industry', label: '行业' } + }, + { + type: SelectAddressFilterItem, + props: { name: 'city', label: '城市' } + } + ] ]} + displayLine={1} /> - + + 当前筛选值: +
{JSON.stringify(filterValue, null, 2)}
+
+ ); }; @@ -409,1548 +221,1142 @@ render(); ``` -- 筛选值展示 -- 展示 FilterValueDisplay、FilterItem、FilterLines、PopoverItem 等组件的独立使用 -- _Filter(@components/Filter),antd(antd) +- 高级筛选 +- 使用 AdvancedFilter 组件实现更复杂的筛选布局 +- _ReactFilter(@kne/react-filter)[import * as _ReactFilter from "@kne/react-filter"],(@kne/react-filter/dist/index.css),antd(antd) ```jsx -const { - FilterValueDisplay, - FilterItem, - FilterLines, - PopoverItem, - InputFilterItem, - CityFilterItem, - AdvancedSelectFilterItem, - UserFilterItem, - FunctionSelectFilterItem, - IndustrySelectFilterItem, -} = _Filter; -const { Space, Input } = antd; +const { AdvancedFilter } = _ReactFilter; +const { InputFilterItem, ListFilterItem, CityFilterItem } = AdvancedFilter.fields; +const { Flex, Button, message } = antd; const { useState } = React; -const BaseExample = () => { - const [value, setValue] = useState([ - { - label: "城市", - name: "city", - value: [ - { label: "上海", value: "010" }, - { label: "北京", value: "020" }, - ], - }, - { - label: "职能", - name: "function", - value: [ - { label: "产品经理", value: "010" }, - { label: "销售", value: "020" }, - { - label: "客户经理", - value: "030", - }, - ], - }, - ]); + +const AdvancedFilterExample = () => { + const [filterValue, setFilterValue] = useState([]); + + const handleSearch = () => { + const params = {}; + filterValue.forEach(item => { + params[item.name] = Array.isArray(item.value) + ? item.value.map(v => v.value) + : item.value?.value; + }); + message.info(`搜索参数: ${JSON.stringify(params, null, 2)}`); + console.log('筛选参数:', params); + }; + return ( - - - - - - - - - + , - , - , - ], - [ - , - , - , + { + type: InputFilterItem, + props: { + name: 'name', + label: '姓名' + } + }, + { + type: InputFilterItem, + props: { + name: 'phone', + label: '手机号' + } + } ], [ - , - , - , + { + type: ListFilterItem, + props: { + name: 'status', + label: '状态', + single: true, + items: [ + { label: '待处理', value: 'pending' }, + { label: '处理中', value: 'processing' }, + { label: '已完成', value: 'completed' }, + { label: '已取消', value: 'cancelled' } + ] + } + } ], [ - , - , - , + { + type: ListFilterItem, + props: { + name: 'tags', + label: '标签', + single: false, + maxLength: 3, + items: [ + { label: '前端', value: 'frontend' }, + { label: '后端', value: 'backend' }, + { label: '全栈', value: 'fullstack' }, + { label: 'UI设计', value: 'ui' }, + { label: '产品', value: 'product' } + ] + } + } ], - ]} - /> - - {({ value, onChange }) => ( - onChange(e.target.value)} /> - )} - - , - , - { - return { - pageData: [ - { label: "第一项", value: 1 }, - { label: "第二项", value: 2, disabled: true }, - { - label: "第三项", - value: 3, - }, - ], - }; - }, - }} - />, - { - return { - pageData: [ - { - label: "用户一", - value: 1, - description: "我是用户描述", - }, - { - label: "用户二", - value: 2, - description: "我是用户描述", - }, - { - label: "用户三", - value: 3, - description: "我是用户描述", - }, - ], - }; - }, - }} - />, - , - , - ], + { + type: CityFilterItem, + props: { + name: 'city', + label: '城市', + maxLength: 3 + } + } + ] ]} /> - + + + + + 当前筛选值: +
+          {JSON.stringify(filterValue, null, 2)}
+        
+
+ ); }; -render(); +render(); ``` -- 数值范围筛选 -- 展示 NumberRangeFilterItem 数值范围筛选组件的使用 -- _Filter(@components/Filter) +- 筛选字段组件 +- 展示所有筛选字段组件类型,包括输入筛选、数字区间、日期选择、下拉选择以及 SuperSelect 业务选择器(职能/行业/城市) +- _ReactFilter(@kne/react-filter)[import * as _ReactFilter from "@kne/react-filter"],(@kne/react-filter/dist/index.css),antd(antd) ```jsx +const { fields, PopoverItem } = _ReactFilter; const { - default: Filter, - NumberRangeFilterItem, - getFilterValue, -} = _Filter; + InputFilterItem, NumberRangeFilterItem, DatePickerFilterItem, + DateRangePickerFilterItem, TypeDateRangePickerFilterItem, + SuperSelectFilterItem, SelectFunctionFilterItem, + SelectIndustryFilterItem, SelectAddressFilterItem +} = fields; +const { Input, InputNumber, Space, Flex, Select, Divider, Tag } = antd; const { useState } = React; -const BaseExample = () => { - const [value, onChange] = useState([]); - +// 自定义下拉选择筛选项 +const SelectFilterItem = ({ label, value, onChange, options = [] }) => { return ( - { - console.log('筛选值:', getFilterValue(value)); - onChange(value); - }} - list={[ - [ - , - , - , - ], - ]} - /> + onChange={onChange} + > + {({ value, onChange }) => ( + onChange({ label, value: val })} - allowClear - style={{ width: 200 }} - options={options} - /> - ); -}); +const FilterFieldsExample = () => { + const [values, setValues] = useState({}); -const BaseExample = () => { - const [value, onChange] = useState([]); + const fieldConfigs = [ + { + name: 'input', + label: '输入筛选', + component: InputFilterItem, + props: {} + }, + { + name: 'numberRange', + label: '数字区间', + component: NumberRangeFilterItem, + props: { unit: '万', min: 0 } + }, + { + name: 'date', + label: '日期选择', + component: DatePickerFilterItem, + props: { picker: 'date' } + }, + { + name: 'month', + label: '月份选择', + component: DatePickerFilterItem, + props: { picker: 'month' } + }, + { + name: 'dateRange', + label: '日期范围', + component: DateRangePickerFilterItem, + props: {} + }, + { + name: 'typeDateRange', + label: '类型日期范围', + component: TypeDateRangePickerFilterItem, + props: {} + }, + { + name: 'select', + label: '下拉选择', + component: SelectFilterItem, + props: { + options: [ + { value: 'pending', label: '待处理' }, + { value: 'processing', label: '处理中' }, + { value: 'completed', label: '已完成' }, + { value: 'cancelled', label: '已取消' } + ] + } + } + ]; return ( - { - console.log('筛选值:', value); - onChange(value); - }} - list={[ - [ - , - , - , - , - ], - ]} - /> + +

筛选字段组件展示

+ + {fieldConfigs.map(({ name, label, component: Component, props }) => ( + setValues(prev => ({ ...prev, [name]: val }))} + {...props} + /> + ))} + + + 当前值: +
+          {JSON.stringify(values, null, 2)}
+        
+
+
); }; -render(); - -``` - -- FilterProvider 和 useFilter -- 展示如何使用 FilterProvider 和 useFilter Hook 自定义筛选界面 -- _Filter(@components/Filter),antd(antd) - -```jsx -const { - FilterProvider, - FilterLines, - FilterValueDisplay, - useFilter, - InputFilterItem, - CityFilterItem, - UserFilterItem, - FunctionSelectFilterItem, - IndustrySelectFilterItem, - DatePickerFilterItem, - NumberRangeFilterItem, -} = _Filter; -const { Space, Card, Button, Modal, Tag, Alert } = antd; -const { useState } = React; - -// 演示 FilterProvider 和 useFilter 的使用 -const CustomFilterContent = () => { - const { value, onChange } = useFilter(); - const [modalVisible, setModalVisible] = useState(false); - - const handleViewFilterValue = () => { - setModalVisible(true); - }; - - const renderFilterValue = () => { - if (!value) { - return

暂无筛选条件

; - } - - // 处理 value 可能是 Map 的情况 - const valueArray = value instanceof Map ? Array.from(value.values()) : (Array.isArray(value) ? value : []); - - if (valueArray.length === 0) { - return

暂无筛选条件

; - } +// SuperSelect 业务选择器示例 +const departmentOptions = [ + { value: 'tech', label: '技术研发部' }, + { value: 'product', label: '产品设计部' }, + { value: 'operation', label: '运营管理部' }, + { value: 'hr', label: '人力资源部' }, + { value: 'finance', label: '财务部' }, + { value: 'marketing', label: '市场营销部' } +]; - return ( - - {valueArray.map((item, index) => ( - - {item.label}: {Array.isArray(item.value) - ? item.value.map(v => v.label).join(', ') - : item.value?.label || item.value} - - ))} - - ); - }; +const SuperSelectExample = () => { + const [values, setValues] = useState({}); return ( - - - - - , - , - , - , - ], - [ - , - , - , - ], + + +

SuperSelect 业务选择器

+ 单选/多选 + 搜索 + 全选 +
+

+ 基于 @kne/super-select 的通用选择器筛选项,支持单选/多选、搜索、全选等功能 +

+ + setValues(prev => ({ ...prev, dept: val }))} + options={departmentOptions} + /> + setValues(prev => ({ ...prev, status: val }))} + options={[ + { value: 'active', label: '启用' }, + { value: 'inactive', label: '停用' } ]} /> -
- - - - setModalVisible(false)} - footer={[ - , - ]} - width={600} - > - {renderFilterValue()} -
- -
-
-
+ setValues(prev => ({ ...prev, role: val }))} + options={[ + { value: 'admin', label: '管理员' }, + { value: 'editor', label: '编辑者' }, + { value: 'viewer', label: '查看者' } + ]} + allowSelectedAll + /> + +
+        {JSON.stringify(values, null, 2)}
+      
+ ); }; -const BaseExample = () => { - const [value, setValue] = useState([]); +// 业务选择器示例(职能/行业/城市) +const BusinessSelectExample = () => { + const [values, setValues] = useState({}); return ( - - - {value.length > 0 && ( - - )} - + + +

业务选择器筛选项

+ 多级数据 + 拼音搜索 + 国际化 +
+

+ 基于 @kne/super-select-plus 的职能、行业、城市选择器,支持多级数据、拼音搜索、国际化 +

+ + setValues(prev => ({ ...prev, function: val }))} + /> + setValues(prev => ({ ...prev, industry: val }))} + /> + setValues(prev => ({ ...prev, city: val }))} + /> + setValues(prev => ({ ...prev, singleCity: val }))} + /> + +
); }; -render(); +render(); +render(); +render(); +render(); +render(); ``` -- 筛选值映射与提取 -- 展示 createFilterValueMapper 声明式值映射和 pickSelectValues 值提取工具的用法 -- _Filter(@components/Filter),antd(antd) +- 已选值展示 +- 使用 FilterValueDisplay 组件展示已选择的筛选条件,支持单独删除和清空全部 +- _ReactFilter(@kne/react-filter)[import * as _ReactFilter from "@kne/react-filter"],(@kne/react-filter/dist/index.css),antd(antd) ```jsx -const { - default: Filter, - SuperSelectFilterItem, - CityFilterItem, - InputFilterItem, - getFilterValue, - pickSelectValues, - createFilterValueMapper, -} = _Filter; +const { FilterValueDisplay } = _ReactFilter; +const { Flex } = antd; const { useState } = React; -const { Space, Card, Divider, Typography } = antd; -const { Text, Title } = Typography; - -// 声明式创建 mapFilterValue 函数 -const mapFilterValue = createFilterValueMapper({ - keyword: 'string', // 确保字符串类型 - city: 'multi', // 多选 → string[] - status: 'single', // 单选 → string -}); - -const BaseExample = () => { - const [value, onChange] = useState([ - { name: 'keyword', label: '关键词', value: { label: '搜索词', value: '搜索词' } }, - { name: 'city', label: '城市', value: [{ label: '上海', value: '010' }, { label: '北京', value: '020' }] }, - { name: 'status', label: '状态', value: [{ label: '启用', value: 'active', id: 'active' }] }, +const FilterValueDisplayExample = () => { + const [filterValue, setFilterValue] = useState([ + { name: 'keyword', label: '关键词', value: { label: 'React', value: 'React' } }, + { name: 'status', label: '状态', value: { label: '已完成', value: 'completed' } }, + { name: 'amount', label: '金额', value: { label: '100-500万', value: [100, 500] } }, + { + name: 'tags', + label: '标签', + value: [ + { label: '前端', value: 'frontend' }, + { label: 'React', value: 'react' } + ] + } ]); - const rawFilterValue = getFilterValue(value); - const mappedFilterValue = mapFilterValue(value, getFilterValue); - return ( - - , - , - , - ], - ]} + +

已选筛选条件展示

+ + 共 {filterValue.length} 项 + + } /> - - - {`pickSelectValues([{ value: 1 }, { id: 2 }, '3'])`} -
- 结果:{JSON.stringify(pickSelectValues([{ value: 1 }, { id: 2 }, '3']))} - - {`pickSelectValues({ value: 'open' })`} -
- 结果:{JSON.stringify(pickSelectValues({ value: 'open' }))} -
- - - 原始 getFilterValue 结果 -
-          {JSON.stringify(rawFilterValue, null, 2)}
-        
- createFilterValueMapper 映射后结果 -
-          {JSON.stringify(mappedFilterValue, null, 2)}
+      
+        当前值:
+        
+          {JSON.stringify(filterValue, null, 2)}
         
- - +
+ ); }; -render(); +render(); ``` -- URL 筛选参数 -- 展示 filterToUrlParams、parseFilterEntry、takeFilterEntry、createUrlFilterReader 等 URL 参数序列化与反序列化工具的用法 -- _Filter(@components/Filter),antd(antd) +- 弹出层筛选 +- 使用 PopoverItem 组件实现弹出层形式的筛选项,支持文本输入、数字输入、下拉选择和数值范围等多种交互形式 +- _ReactFilter(@kne/react-filter)[import * as _ReactFilter from "@kne/react-filter"],(@kne/react-filter/dist/index.css),antd(antd) ```jsx -const { - default: Filter, - InputFilterItem, - CityFilterItem, - SuperSelectFilterItem, - filterToUrlParams, - parseFilterEntry, - takeFilterEntry, - createUrlFilterReader, - getFilterValue, - createFilterValueMapper, - pickSelectValues, - useUrlFilterValue, -} = _Filter; -const { useState, useMemo } = React; -const { Space, Card, Divider, Typography, Button, Alert, Tag } = antd; - -const { Text, Title, Paragraph } = Typography; - -// ========== 示例数据 ========== -const sampleFilterValue = [ - { name: 'keyword', label: '关键词', value: { label: '前端开发', value: '前端开发' } }, - { name: 'city', label: '城市', value: [{ label: '上海', value: '010' }, { label: '北京', value: '020' }] }, - { name: 'status', label: '状态', value: { label: '招聘中', value: 'active', id: 'active' } }, -]; - -// 声明式创建 mapFilterValue,配合 URL 参数使用 -const mapFilterValue = createFilterValueMapper({ - keyword: 'string', - city: 'multi', - status: 'single', -}); - -const BaseExample = () => { - const [value, onChange] = useState([]); - - // ===== 1. filterToUrlParams:筛选值 → URL 参数 ===== - const sampleUrlParams = useMemo(() => filterToUrlParams(sampleFilterValue), []); - const liveUrlParams = useMemo(() => filterToUrlParams(value), [value]); - - // ===== 2. parseFilterEntry:解析单个值 ===== - const parsedEntries = useMemo(() => [ - { input: "'前端开发'", output: parseFilterEntry('前端开发') }, - { input: "'招聘中:active'", output: parseFilterEntry('招聘中:active') }, - { input: "'上海:010'", output: parseFilterEntry('上海:010') }, - ], []); - - // ===== 3. 从 URL 参数反序列化还原筛选状态 ===== - const roundTripResult = useMemo(() => { - const urlStr = sampleUrlParams.toString(); - const searchParams = new URLSearchParams(urlStr); - - // 使用 createUrlFilterReader 读取 - const reader = createUrlFilterReader(searchParams); - const keyword = reader.takeFilterEntry('keyword'); - const city = reader.takeFilterEntry('city', { multi: true }); - const status = reader.takeFilterEntry('status'); - const consumedKeys = reader.getConsumedKeys(); - - // 还原为 filter 数组 - const restored = []; - if (keyword) restored.push({ name: 'keyword', label: '关键词', value: keyword }); - if (city) restored.push({ name: 'city', label: '城市', value: city }); - if (status) restored.push({ name: 'status', label: '状态', value: status }); - - return { urlStr, keyword, city, status, consumedKeys, restored }; - }, [sampleUrlParams]); - - // ===== 4. takeFilterEntry 直接读取 ===== - const takeResults = useMemo(() => { - const sp = sampleUrlParams; - return [ - { input: "takeFilterEntry(params, 'keyword')", output: takeFilterEntry(sp, 'keyword') }, - { input: "takeFilterEntry(params, 'city', { multi: true })", output: takeFilterEntry(sp, 'city', { multi: true }) }, - { input: "takeFilterEntry(params, 'status')", output: takeFilterEntry(sp, 'status') }, - ]; - }, [sampleUrlParams]); - - // ===== 5. 映射后筛选值 ===== - const mappedSample = mapFilterValue(sampleFilterValue, getFilterValue); - - return ( - - - - {/* ===== 交互式 Filter ===== */} - - 选择筛选条件后,下方 URL 参数会实时更新 - , - , - , - ], - ]} - /> - {value.length > 0 && ( - <> - - 当前筛选值 -
-              {JSON.stringify(value, null, 2)}
-            
- 序列化后的 URL 参数 -
-              {liveUrlParams.toString() || '(无)'}
-            
- - )} -
- - {/* ===== filterToUrlParams ===== */} - - - 将筛选值数组序列化为 URLSearchParams,保留 label 信息以便反序列化还原 - - 输入:筛选值数组 -
-{JSON.stringify(sampleFilterValue, null, 2)}
-        
- 输出:URL 参数字符串 -
-          {roundTripResult.urlStr}
-        
-
- - {/* ===== parseFilterEntry ===== */} - - - 将 URL 参数中的字符串反序列化为 {`{ label, value }`} 对象 - - {parsedEntries.map(({ input, output }, i) => ( -
- {`parseFilterEntry(${input})`} - {' → '} - {JSON.stringify(output)} -
- ))} -
- - {/* ===== takeFilterEntry ===== */} - - - 直接从 URLSearchParams 中读取并反序列化指定 key 的筛选值 - - {takeResults.map(({ input, output }, i) => ( -
- {input} -
- 结果: - {JSON.stringify(output)} -
- ))} -
- - {/* ===== createUrlFilterReader + 完整还原流程 ===== */} - - - 使用 createUrlFilterReader 从 URL 参数读取并还原完整的筛选状态,同时追踪已消费的 key - - 步骤1:从 URL 参数读取各字段值 -
-{`const reader = createUrlFilterReader(searchParams);
-const keyword = reader.takeFilterEntry('keyword');     // → ${JSON.stringify(roundTripResult.keyword)}
-const city    = reader.takeFilterEntry('city', { multi: true }); // → ${JSON.stringify(roundTripResult.city)}
-const status  = reader.takeFilterEntry('status');      // → ${JSON.stringify(roundTripResult.status)}
-reader.getConsumedKeys();                               // → ${JSON.stringify(roundTripResult.consumedKeys)}`}
-        
- 步骤2:还原为筛选值数组 -
-          {JSON.stringify(roundTripResult.restored, null, 2)}
-        
- - 验证:还原后的数据与原始数据一致 - - 还原成功 - - keyword: {roundTripResult.keyword?.value} | city: [{roundTripResult.city?.map(c => c.value).join(', ')}] | status: {roundTripResult.status?.value} - - -
- - {/* ===== createFilterValueMapper ===== */} - - - 实际业务中,先用 createFilterValueMapper 将筛选值映射为接口参数格式,再用 filterToUrlParams 序列化到 URL - - 映射后的筛选值(用于接口请求) -
-{`const mapFilterValue = createFilterValueMapper({
-  keyword: 'string',   // 确保字符串
-  city: 'multi',       // 多选 → string[]
-  status: 'single',    // 单选 → string
-});
-
-mapFilterValue(filterValue, getFilterValue);
-// →`}
-{'  ' + JSON.stringify(mappedSample, null, 2)}
-        
-
- - {/* ===== useUrlFilterValue ===== */} - - - 基于 useUrlFilter 封装的简化版 Hook,使用 createUrlFilterReader 解析 filterParams[key] 格式,自动解析 label:value,支持单选和多选 - - - 1. 数组形式 — 默认单选 -
-{`const [filter, setFilter] = useUrlFilterValue(['keyword', 'status']);
-
-// URL: ?filterParams[keyword]=前端开发&filterParams[status]=招聘中:active
-// → filter: [
-//     { name: 'keyword', value: { label: '前端开发', value: '前端开发' } },
-//     { name: 'status', value: { label: '招聘中', value: 'active' } }
-//   ]`}
-        
- - - - 2. 对象形式 — 多选 + 自定义转换 -
-{`// { multi: true } 表示多选,value 为数组
-// 函数接收解析后的值,返回 filter 项或 null 跳过
-const [filter, setFilter] = useUrlFilterValue({
-  keyword: true,                   // 单选,默认转换
-  city: { multi: true },           // 多选
-  status: (parsed) => parsed       // 自定义:直接用解析值
-    ? { name: 'status', value: parsed }
-    : null
-});
-
-// URL: ?filterParams[city]=上海:010,北京:020
-// → city 的 value: [{ label: '上海', value: '010' }, { label: '北京', value: '020' }]`}
-        
- - - - 对比 useUrlFilter -
-{`// useUrlFilter(完整控制,需手动解析)
-const [filter, setFilter] = useUrlFilter({
-  readUrlParams: (searchParams) => {
-    const { takeFilterEntry, getConsumedKeys } = createUrlFilterReader(searchParams);
-    const keyword = takeFilterEntry('keyword');
-    const city = takeFilterEntry('city', { multi: true });
-    return { consumedKeys: getConsumedKeys(), keyword, city };
-  },
-  buildFilter: ({ keyword, city }) => [
-    ...(keyword ? [{ name: 'keyword', value: keyword }] : []),
-    ...(city ? [{ name: 'city', value: city }] : []),
-  ]
-});
-
-// useUrlFilterValue(等价简化写法)
-const [filter, setFilter] = useUrlFilterValue({
-  keyword: true,
-  city: { multi: true }
-});`}
-        
-
-
- ); -}; - -render(); - -``` - -- 筛选值拦截器 -- 展示 filterInterceptors、singleSelectInterceptor、multiSelectInterceptor 拦截器的用法,解决 SuperSelect 组件 { id, name } 与 Filter 上下文 { label, value } 格式不匹配的问题 -- _Filter(@components/Filter),antd(antd) - -```jsx -const { - default: Filter, - SuperSelectFilterItem, - filterInterceptors, - singleSelectInterceptor, - multiSelectInterceptor, - getFilterValue, - pickSelectValues, -} = _Filter; +const { PopoverItem } = _ReactFilter; +const { Input, InputNumber, Space, Select, Radio, Flex } = antd; const { useState } = React; -const { Space, Card, Divider, Typography, Alert } = antd; - -const { Text, Title, Paragraph } = Typography; -// filterInterceptors 提供了 single 和 multi 两种预设拦截器 -// 适用于 SuperSelectFilterItem 等使用 { id, name } 格式的组件 -// interceptor.input:将 { label, value } 转为 { id, name }(传入组件的 value 格式) -// interceptor.output:将 { id, name } 转回 { label, value }(筛选上下文的 value 格式) - -const BaseExample = () => { - const [value1, onChange1] = useState([]); - const [value2, onChange2] = useState([]); +const PopoverItemExample = () => { + const [inputValue, setInputValue] = useState(null); + const [numberValue, setNumberValue] = useState(null); + const [selectValue, setSelectValue] = useState(null); + const [rangeValue, setRangeValue] = useState(null); return ( - - - - - - 适用于 valueKey="id" labelKey="name" 的单选 SuperSelect 场景,自动在 {`{label, value}`} 和 {`{id, name}`} 之间转换 - - { - console.log('筛选值:', getFilterValue(value)); - onChange1(value); - }} - list={[ - [ - , - , - ], - ]} - /> - {value1.length > 0 && ( - <> - - 当前筛选值 -
-              {JSON.stringify(getFilterValue(value1), null, 2)}
-            
- pickSelectValues 提取原始值 -
-              {JSON.stringify(Object.fromEntries(
-                value1.map(item => [item.name, pickSelectValues(item.value)])
-              ), null, 2)}
-            
- - )} -
- - - - 适用于 valueKey="id" labelKey="name" 的多选 SuperSelect 场景,自动在 {`[{label, value}]`} 和 {`[{id, name}]`} 之间转换 - - { - console.log('筛选值:', getFilterValue(value)); - onChange2(value); + +

弹出层筛选组件示例

+ + {/* 输入框筛选 */} + + {({ value, onChange }) => ( + onChange( + e.target.value ? { label: e.target.value, value: e.target.value } : null + )} + /> + )} + + + {/* 数字输入筛选 */} + val?.value !== undefined} + > + {({ value, onChange }) => ( + onChange( + val !== null ? { label: String(val), value: val } : null + )} + /> + )} + + + {/* 下拉选择筛选 */} + + {({ value, onChange }) => ( + + onChange({ + label: `${value?.value?.[0] || '?'}-${val || '?'}`, + value: [value?.value?.[0], val] + })} + /> + + )} + + + + +
当前值:
+
+          {JSON.stringify({
+            文本输入: inputValue,
+            数字输入: numberValue,
+            状态选择: selectValue,
+            数值范围: rangeValue
+          }, null, 2)}
         
-
-
+ + ); }; -render(); +render(); ``` ### API -### Filter 组件 +### Filter 主组件 + +筛选组件,用于展示筛选项和处理筛选条件。 + +#### 属性 + +| 属性 | 类型 | 默认值 | 说明 | +|--------------|------------------------------------------------------|--------|-----------------| +| value | `Array<{ name: string, label: string, value: any }>` | - | 筛选值数组 | +| defaultValue | `Array<{ name: string, label: string, value: any }>` | `[]` | 默认筛选值 | +| onChange | `(value: Array) => void` | - | 筛选值变化回调 | +| list | `Array` | `[]` | 筛选项配置数组,支持多行 | +| displayLine | `number` | `1` | 默认展示的行数,超出部分折叠 | +| label | `string` | `'筛选'` | 筛选区域标题 | +| extra | `ReactNode` | - | 额外操作区域,通常放置搜索按钮 | +| extraExpand | `ReactNode` | - | 已选区域额外内容 | +| className | `string` | - | 自定义类名 | + +#### 静态方法 + +| 方法 | 说明 | +|-------------------------------------------------|-----------------------------------------------------------------------| +| `Filter.getFilterValue(filterValue)` | 将筛选值数组转换为参数对象,如 `{ name: value }` | +| `Filter.useFilter()` | 获取 Filter Context,返回 `{ value, onChange }` | +| `Filter.pickSelectValues(value)` | 从筛选值中提取原始值数组,支持 `{ value }`、`{ id }` 格式 | +| `Filter.createFilterValueMapper(fieldMappers)` | 声明式创建 mapFilterValue 函数,按字段映射转换规则 | +| `Filter.filterToUrlParams(filterValue, options)`| 将筛选值数组序列化为 URLSearchParams,保留 label 信息 | +| `Filter.parseFilterEntry(str)` | 解析 URL 参数中的单个筛选值项为 `{ label, value }` | +| `Filter.takeFilterEntry(searchParams, key, options)` | 从 URL 参数中读取筛选值项,支持单选/多选 | +| `Filter.createUrlFilterReader(searchParams)` | 创建 URL 筛选参数读取器,自动追踪已消费的参数 key | +| `Filter.useUrlFilter(options)` | 从 URL 参数初始化 Filter 状态的 hook(React Router 环境) | +| `Filter.useUrlFilterValue(mapping)` | useUrlFilter 的简化版,基于 filterParams[key] 格式自动解析 URL 参数 | +| `Filter.createUrlParamsReader(searchParams)` | 创建通用 URL 参数读取器,自动追踪已消费的参数 key | +| `Filter.stripConsumedUrlParams(searchParams, consumedKeys)` | 从 URL 参数中移除已消费的 key,返回新的 URLSearchParams 或 null | +| `Filter.filterInterceptors.single` | 单选拦截器:`{id, name}` ↔ `{label, value}` 数据格式转换 | +| `Filter.filterInterceptors.multi` | 多选拦截器:`[{id, name}]` ↔ `[{label, value}]` 数据格式转换 | + +#### 使用示例 -筛选组件的主入口,用于构建灵活的筛选条件界面。支持受控和非受控模式,自动管理筛选值状态。 +```javascript +import Filter, { fields } from '@kne/react-filter'; + +const { InputFilterItem, NumberRangeFilterItem } = fields; + +搜索} +/> +``` + +--- + +### AdvancedFilter 高级筛选组件 -#### 组件属性 +高级筛选组件,用于更复杂的筛选场景,采用垂直布局。 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| value | 筛选值数组,受控模式使用 | array | 否 | [] | -| onChange | 筛选值变化回调函数 | function | 否 | - | -| defaultValue | 默认筛选值 | array | 否 | [] | -| list | 筛选项配置数组,支持多行布局 | array | 否 | - | -| displayLine | 默认显示的行数,超过则支持展开/收起 | number | 否 | 1 | -| label | 筛选标题,默认显示"筛选" | ReactNode | 否 | - | -| extra | 额外内容,通常用于放置搜索输入框等 | ReactNode | 否 | - | -| extraExpand | 额外展开内容,显示在已选筛选值右侧 | ReactNode | 否 | - | -| className | 自定义类名 | string | 否 | - | +#### 属性 -#### value 数据结构 +| 属性 | 类型 | 默认值 | 说明 | +|--------------|------------------------------------------------------|------|----------| +| value | `Array<{ name: string, label: string, value: any }>` | - | 筛选值数组 | +| defaultValue | `Array<{ name: string, label: string, value: any }>` | `[]` | 默认筛选值 | +| onChange | `(value: Array) => void` | - | 筛选值变化回调 | +| list | `Array` | `[]` | 筛选项配置数组 | +| more | `Array` | - | 额外折叠的筛选项 | +| className | `string` | - | 自定义类名 | -筛选值数组中每个元素的结构: +#### 使用示例 ```javascript -{ - name: 'city', // 筛选字段名 - label: '城市', // 筛选项标签 - value: [ // 筛选值,可以是单个值或数组 - { label: '上海', value: '010' }, - { label: '北京', value: '020' } - ] -} +import { AdvancedFilter, fields } from '@kne/react-filter'; + + ``` -### AdvancedFilter 组件 +--- -高级筛选组件,适用于需要展示多个筛选条件且支持展开/收起的场景。相比普通 Filter 组件,提供更紧凑的布局。 +### FilterValueDisplay 已选值展示 -#### 组件属性 +展示已选择的筛选条件,支持单独删除和清空全部。 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| value | 筛选值数组 | array | 否 | [] | -| onChange | 筛选值变化回调函数 | function | 否 | - | -| defaultValue | 默认筛选值 | array | 否 | [] | -| list | 筛选项配置数组 | array | 否 | - | -| more | 更多筛选项,支持展开/收起 | ReactNode | 否 | - | +#### 属性 -### FilterLines 组件 +| 属性 | 类型 | 默认值 | 说明 | +|-------------|------------------------------------------------------|-----|---------| +| value | `Array<{ name: string, label: string, value: any }>` | - | 筛选值数组 | +| onChange | `(value: Array) => void` | - | 筛选值变化回调 | +| extraExpand | `ReactNode` | - | 额外展示内容 | -筛选项布局组件,用于按照行展示筛选项,支持展开/收起功能。 +--- -#### 组件属性 +### PopoverItem 弹出层筛选项 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| list | 筛选项配置数组,支持多行布局 | array | 否 | - | -| displayLine | 默认显示的行数 | number | 否 | 1 | -| label | 筛选标题 | ReactNode | 否 | - | -| extra | 额外内容 | ReactNode | 否 | - | -| className | 自定义类名 | string | 否 | - | +弹出层形式的筛选项,支持确认取消操作。 -### FilterValueDisplay 组件 +#### 属性 -筛选值展示组件,用于展示已选择的筛选条件,支持单个删除和清空全部。 +| 属性 | 类型 | 默认值 | 说明 | +|------------------|---------------------------------------------|----------------|-----------| +| label | `string` | - | 筛选项标签 | +| value | `{ label: string, value: any }` | - | 当前值 | +| onChange | `(value: object) => void` | - | 值变化回调 | +| onValidate | `(value: object) => boolean` | - | 确认按钮校验函数 | +| onOpenChange | `(open: boolean) => void` | - | 弹出层状态变化回调 | +| placement | `string` | `'bottomLeft'` | 弹出层位置 | +| overlayClassName | `string` | - | 弹出层自定义类名 | +| children | `(props: { value, onChange }) => ReactNode` | - | 内容渲染函数 | -#### 组件属性 +#### 使用示例 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| value | 筛选值数组 | array | 是 | - | -| onChange | 筛选值变化回调函数 | function | 是 | - | -| extraExpand | 额外展开内容,显示在清空按钮左侧 | ReactNode | 否 | - | +```javascript +import { PopoverItem } from '@kne/react-filter'; + + + {({ value, onChange }) => ( + onChange({ label: e.target.value, value: e.target.value })} + /> + )} + +``` -### FilterItem 组件 +--- -筛选项容器组件,提供统一的样式和交互效果。 +### FilterItem 筛选项容器 -#### 组件属性 +筛选项的基础容器组件。 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 筛选项标签 | ReactNode | 否 | - | -| open | 是否打开下拉 | boolean | 否 | - | -| active | 是否激活状态(有选中值时自动激活) | boolean | 否 | - | -| children | 筛选项内容 | ReactNode | 否 | - | +#### 属性 -### PopoverItem 组件 +| 属性 | 类型 | 默认值 | 说明 | +|----------|-------------|-----|------------| +| label | `string` | - | 筛选项标签 | +| open | `boolean` | - | 是否展开状态 | +| active | `boolean` | - | 是否激活状态(有值) | +| children | `ReactNode` | - | 子元素 | -弹出式筛选项组件,基于 Popover 实现。 +--- -#### 组件属性 +### FilterLines 筛选行 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 筛选项标签 | ReactNode | 否 | - | -| name | 字段名 | string | 否 | - | -| children | 渲染函数,接收 { value, onChange } 参数 | function | 是 | - | +筛选行组件,支持多行展开收起。 -### SearchInput 组件 +#### 属性 -搜索输入框组件,通常用于放置在筛选器右侧的搜索功能。 +| 属性 | 类型 | 默认值 | 说明 | +|-------------|----------------|--------|---------| +| list | `Array` | `[]` | 筛选项配置数组 | +| displayLine | `number` | `1` | 默认展示行数 | +| label | `string` | `'筛选'` | 标题 | +| extra | `ReactNode` | - | 额外操作区域 | +| className | `string` | - | 自定义类名 | -#### 组件属性 +--- -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| placeholder | 输入框占位符 | string | 否 | - | -| value | 输入框值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +### FilterProvider 状态管理 -### FilterItemContainer 组件 +Filter 状态管理组件,用于自定义 Filter 结构。 -筛选项容器组件,用于包装需要传递额外属性的筛选项。 +#### 属性 -#### 组件属性 +| 属性 | 类型 | 默认值 | 说明 | +|--------------|---------------------------------------|------|----------| +| value | `Array` | - | 筛选值数组 | +| defaultValue | `Array` | `[]` | 默认筛选值 | +| onChange | `(value: Array) => void` | - | 筛选值变化回调 | +| children | `ReactNode \| (context) => ReactNode` | - | 子元素或渲染函数 | -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| name | 字段名 | string | 否 | - | -| label | 字段标签 | string | 否 | - | -| children | 渲染函数,接收筛选项 props | function | 是 | - | +--- -### InputFilterItem 组件 +### 高阶组件 -文本输入筛选项组件,用于文本类型的筛选。 +#### withFilterValue -#### 组件属性 +为组件注入筛选值和变更函数。 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| placeholder | 输入框占位符 | string | 否 | 请输入{label} | -| value | 输入框值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +```javascript +import { withFilterValue } from '@kne/react-filter'; -### DatePickerFilterItem 组件 +const MyFilterItem = withFilterValue(({ name, label, value, onChange, ...props }) => { + return ; +}); +``` -日期选择筛选项组件,支持多种日期选择模式。 +#### withFieldItem -#### 组件属性 +为组件包装 FilterItem 样式。 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| picker | 日期选择器类型,可选值:`date`、`week`、`month`、`quarter`、`year` | string | 否 | `date` | -| value | 日期值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +```javascript +import { withFieldItem } from '@kne/react-filter'; -### DateRangePickerFilterItem 组件 +const MyFieldItem = withFieldItem(MyComponent); +``` -日期范围选择筛选项组件,用于选择日期范围。 +--- -#### 组件属性 +### 筛选字段组件 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| value | 日期范围值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +#### InputFilterItem 输入筛选 -### TypeDateRangePickerFilterItem 组件 +弹出层形式的输入框筛选组件。 -类型化日期范围选择筛选项组件,支持日期类型选择(如创建时间、更新时间等)。 +| 属性 | 类型 | 默认值 | 说明 | +|-------------|----------------------|-----|--------| +| name | `string` | - | 字段名称 | +| label | `string` | - | 标签 | +| placeholder | `string` | - | 占位符 | +| onValidate | `(value) => boolean` | - | 确认校验函数 | -#### 组件属性 +#### NumberRangeFilterItem 数字区间筛选 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| allowEmpty | 是否允许清空 [开始时间清空, 结束时间清空] | array | 否 | [false, false] | -| value | 日期范围值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +数字区间输入筛选组件。 -### CityFilterItem 组件 +| 属性 | 类型 | 默认值 | 说明 | +|-------------|----------|-----|------| +| name | `string` | - | 字段名称 | +| label | `string` | - | 标签 | +| unit | `string` | - | 单位 | +| min | `number` | - | 最小值 | +| max | `number` | - | 最大值 | +| placeholder | `string` | - | 占位符 | -城市选择筛选项组件,支持省市区三级联动选择。 +#### DatePickerFilterItem 日期筛选 -#### 组件属性 +日期选择筛选组件。 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| value | 城市值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | -| single | 是否单选 | boolean | 否 | false | +| 属性 | 类型 | 默认值 | 说明 | +|--------|------------------------------------------------------|----------------|-------| +| name | `string` | - | 字段名称 | +| label | `string` | - | 标签 | +| picker | `'date' \| 'week' \| 'month' \| 'quarter' \| 'year'` | `'date'` | 选择器类型 | +| format | `string` | `'YYYY-MM-DD'` | 日期格式 | -### NumberRangeFilterItem 组件 +#### DateRangePickerFilterItem 日期范围筛选 -数值范围筛选项组件,用于选择数值范围。 +日期范围选择筛选组件。 -#### 组件属性 +| 属性 | 类型 | 默认值 | 说明 | +|--------|-------------------------------------|----------------|------| +| name | `string` | - | 字段名称 | +| label | `string` | - | 标签 | +| format | `string` | `'YYYY-MM-DD'` | 日期格式 | +| header | `ReactNode \| (props) => ReactNode` | - | 头部内容 | -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| unit | 数值单位 | string | 否 | - | -| value | 数值范围值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +#### TypeDateRangePickerFilterItem 类型日期范围筛选 -### AdvancedSelectFilterItem 组件 +支持按日/周/月切换的日期范围选择筛选组件。 -高级选择筛选项组件,支持远程数据加载、分页、搜索等功能。 +| 属性 | 类型 | 默认值 | 说明 | +|--------|----------|----------------|------| +| name | `string` | - | 字段名称 | +| label | `string` | - | 标签 | +| format | `string` | `'YYYY-MM-DD'` | 日期格式 | -#### 组件属性 +#### SuperSelectFilterItem 通用选择器筛选 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| api | API 配置对象 | object | 否 | - | -| api.loader | 数据加载函数 | function | 否 | - | -| value | 选择值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +基于 `@kne/super-select` 的通用选择器筛选项,支持单选/多选、搜索、全选等功能。 -### SuperSelectFilterItem 组件 +| 属性 | 类型 | 默认值 | 说明 | +|------------------|-----------------------------|---------|----------| +| name | `string` | - | 字段名称 | +| label | `string` | - | 标签 | +| options | `Array<{ value, label }>` | - | 选项数据 | +| single | `boolean` | `false` | 是否单选 | +| allowSelectedAll | `boolean` | `false` | 是否支持全选 | +| maxLength | `number` | - | 最多可选数量 | -超级选择筛选项组件,支持展示描述信息、图标等丰富的选项内容。 +**使用示例:** -#### 组件属性 +```javascript +import { SuperSelectFilterItem } from '@kne/react-filter'; + +// 多选 + + +// 单选 + +``` -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| options | 选项数据数组 | array | 否 | - | -| interceptor | 值转换拦截器,用于在组件内部格式和筛选上下文格式之间转换 | object | 否 | - | -| interceptor.input | 输入拦截器,将上下文值转为组件内部格式 | function | 否 | - | -| interceptor.output | 输出拦截器,将组件内部值转为上下文格式 | function | 否 | - | -| value | 选择值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +> 注意:需要安装 `@kne/super-select` 依赖。 -### SuperSelectTableListFilterItem 组件 +#### SelectFunctionFilterItem 职能筛选 -超级表格列表选择筛选项组件,以表格形式展示选项,支持展示多列信息。 +基于 `@kne/super-select-plus` 的职能选择器筛选项,支持多级职能数据选择、拼音搜索。 -#### 组件属性 +| 属性 | 类型 | 默认值 | 说明 | +|-----------|-----------|---------|----------| +| name | `string` | - | 字段名称 | +| label | `string` | - | 标签 | +| single | `boolean` | `false` | 是否单选 | +| maxLength | `number` | - | 最多可选数量 | -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| interceptor | 值转换拦截器 | object | 否 | - | -| interceptor.input | 输入拦截器 | function | 否 | - | -| interceptor.output | 输出拦截器 | function | 否 | - | -| value | 选择值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +> 注意:需要安装 `@kne/super-select-plus` 依赖。 -### UserFilterItem 组件 +#### SelectIndustryFilterItem 行业筛选 -用户选择筛选项组件,专门用于用户选择场景。 +基于 `@kne/super-select-plus` 的行业选择器筛选项,支持多级行业数据选择、拼音搜索。 -#### 组件属性 +| 属性 | 类型 | 默认值 | 说明 | +|-----------|-----------|---------|----------| +| name | `string` | - | 字段名称 | +| label | `string` | - | 标签 | +| single | `boolean` | `false` | 是否单选 | +| maxLength | `number` | - | 最多可选数量 | -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| api | API 配置对象 | object | 否 | - | -| api.loader | 数据加载函数 | function | 否 | - | -| value | 用户值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +> 注意:需要安装 `@kne/super-select-plus` 依赖。 -### SuperSelectUserFilterItem 组件 +#### SelectAddressFilterItem 城市筛选 -超级用户选择筛选项组件,支持展示用户描述信息。 +基于 `@kne/super-select-plus` 的城市选择器筛选项,支持国内外城市搜索选择。 -#### 组件属性 +| 属性 | 类型 | 默认值 | 说明 | +|-----------|-----------|---------|----------| +| name | `string` | - | 字段名称 | +| label | `string` | - | 标签 | +| single | `boolean` | `false` | 是否单选 | +| maxLength | `number` | - | 最多可选数量 | -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| api | API 配置对象 | object | 否 | - | -| api.loader | 数据加载函数 | function | 否 | - | -| value | 用户值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +> 注意:需要安装 `@kne/super-select-plus` 依赖。 -### FunctionSelectFilterItem 组件 +#### CityFilterItem(高级筛选) -职能选择筛选项组件,用于选择职能信息。 +城市选择器的高级筛选版本,用于 `AdvancedFilter` 组件的 `list` 配置中。展示热门城市标签,支持搜索选择其他城市。 -#### 组件属性 +| 属性 | 类型 | 默认值 | 说明 | +|-----------|-----------|---------|------------| +| single | `boolean` | `false` | 是否单选 | +| maxLength | `number` | `5` | 最多可选数量 | -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| selectLevel | 选择层级 | number | 否 | - | -| maxLength | 最大选择数量 | number | 否 | - | -| single | 是否单选 | boolean | 否 | false | -| onlyAllowLastLevel | 是否只允许选择最后一级 | boolean | 否 | false | -| value | 职能值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +**在高级筛选中使用:** -### IndustrySelectFilterItem 组件 +```javascript +import { AdvancedFilter } from '@kne/react-filter'; +import { CityFilterItem } from './AdvancedFilter/fields'; + + +``` -行业选择筛选项组件,用于选择行业信息。 +--- -#### 组件属性 +### TypeDateRangePickerField 类型日期范围选择器 -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| selectLevel | 选择层级 | number | 否 | - | -| maxLength | 最大选择数量 | number | 否 | - | -| single | 是否单选 | boolean | 否 | false | -| onlyAllowLastLevel | 是否只允许选择最后一级 | boolean | 否 | false | -| value | 行业值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +支持按日/周/月切换的日期范围选择器基础组件。 -### CascaderFilterItem 组件 +| 属性 | 类型 | 默认值 | 说明 | +|-----------------|------------------------------------------------------------|---------------------------------|----------| +| value | `{ type: string, value: [Date, Date] }` | - | 当前值 | +| defaultValue | `{ type: string, value: [Date, Date] }` | `{ type: 'date', value: null }` | 默认值 | +| onChange | `(value: object) => void` | - | 值变化回调 | +| shortcuts | `boolean` | `true` | 是否显示快捷选项 | +| shortcutOptions | `Array<{ label: string, getValue: () => [Dayjs, Dayjs] }>` | - | 自定义快捷选项 | -级联选择筛选项组件,用于级联数据的选择。 +**value 结构:** -#### 组件属性 +```typescript +interface TypeDateRangeValue { + type: 'date' | 'week' | 'month'; // 日期类型 + value: [Date, Date] | null; // 日期范围 [开始时间, 结束时间] +} +``` -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| options | 选项数据数组 | array | 否 | - | -| value | 选择值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +**默认快捷选项:** -### TreeFilterItem 组件 +- 近7天:`dayjs().subtract(7, 'day')` 至今天 +- 本月:本月第一天至最后一天 +- 近三个月:`dayjs().subtract(3, 'month')` 至今天 +- 当年:本年第一天至最后一天 -树形选择筛选项组件,用于树形结构数据的选择。 +**自定义快捷选项示例:** -#### 组件属性 +```javascript +import { TypeDateRangePickerField } from '@kne/react-filter'; -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| api | API 配置对象 | object | 否 | - | -| api.loader | 数据加载函数 | function | 否 | - | -| fieldNames | 字段名映射 | object | 否 | - | -| fieldNames.title | 标题字段名 | string | 否 | `title` | -| fieldNames.key | 键字段名 | string | 否 | `key` | -| fieldNames.children | 子节点字段名 | string | 否 | `children` | -| single | 是否单选 | boolean | 否 | false | -| value | 选择值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | + [dayjs().subtract(7, 'day').startOf('day'), dayjs().endOf('day')] + }, + { + label: '最近一月', + getValue: () => [dayjs().subtract(1, 'month').startOf('day'), dayjs().endOf('day')] + } + ]} +/> +``` -### 高级字段组件 +--- -#### ListFilterItem 组件(AdvancedFilter.fields) +### SearchInput 搜索输入 -列表选择筛选项组件,以标签形式展示选项,支持单选和多选。 +搜索输入组件。 -#### 组件属性 +| 属性 | 类型 | 默认值 | 说明 | +|-------------|----------|-----|------| +| name | `string` | - | 字段名称 | +| label | `string` | - | 标签 | +| placeholder | `string` | - | 占位符 | -| 属性名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| label | 字段标签 | string | 否 | - | -| name | 字段名 | string | 否 | - | -| items | 选项数据数组 | array | 否 | - | -| single | 是否单选 | boolean | 否 | false | -| maxLength | 最大选择数量 | number | 否 | 5 | -| custom | 自定义筛选项 | object | 否 | - | -| value | 选择值 | object | 否 | - | -| onChange | 值变化回调函数 | function | 否 | - | +--- -### 工具函数 +### 工具方法 #### getFilterValue -将筛选值数组转换为对象格式。 +将筛选值数组转换为参数对象。 -**函数签名:** ```javascript -getFilterValue(filterValue: array): object -``` - -**参数说明:** - -| 参数名 | 说明 | 类型 | -|--------|------|------| -| filterValue | 筛选值数组 | array | - -**返回值:** +import { getFilterValue } from '@kne/react-filter'; -转换后的对象,格式为 `{ name: value, ... }`,其中 value 是提取的值数组或单个值。 - -**示例:** -```javascript const filterValue = [ - { name: 'city', value: [{ label: '上海', value: '010' }] }, - { name: 'text', value: { label: '测试', value: 'test' } } + { name: 'keyword', value: { label: 'test', value: 'test' } }, + { name: 'status', value: [{ label: '已完成', value: 'done' }] } ]; -const result = getFilterValue(filterValue); -// result: { city: ['010'], text: 'test' } + +const params = getFilterValue(filterValue); +// { keyword: 'test', status: ['done'] } ``` -#### useFilter +--- -获取筛选上下文的 Hook。 +### 筛选值结构 -**函数签名:** -```javascript -useFilter(): object +筛选值数组中的每一项结构: + +```typescript +interface FilterValueItem { + name: string; // 字段名称 + label: string; // 字段标签(用于展示) + value: { // 单个值 + label: string; // 显示文本 + value: any; // 实际值 + } | Array<{ // 或多个值 + label: string; + value: any; + }> | null; // 或空值 +} ``` -**返回值:** +--- -| 属性名 | 说明 | 类型 | -|--------|------|------| -| value | 筛选值 Map | Map | -| onChange | 筛选值变化函数 | function | +### URL 参数相关 -#### withFilterValue - -高阶组件,用于自动连接筛选上下文。 +#### filterToUrlParams -**函数签名:** -```javascript -withFilterValue(WrappedComponent): ReactComponent -``` +将筛选值数组序列化为 URLSearchParams,保留 label 信息以便反序列化还原完整筛选状态。 -**参数说明:** +| 参数 | 类型 | 默认值 | 说明 | +|----------------|----------------|-----------------|--------------------------| +| filterValue | `Array` | - | 筛选值数组 | +| options.prefix | `string` | `'filterParams'` | URL 参数前缀,设为空字符串则不加前缀 | -| 参数名 | 说明 | 类型 | -|--------|------|------| -| WrappedComponent | 被包装的组件 | ReactComponent | +**序列化格式**: +- 单值且 `label === value`:`prefix[name]=value`(如输入框) +- 单值且 `label !== value`:`prefix[name]=label:value` +- 多值:`prefix[name]=label1:value1,label2:value2` -**新增 Props:** +**使用示例:** -| 属性名 | 说明 | 类型 | -|--------|------|------| -| name | 字段名 | string | -| label | 字段标签 | string | +```javascript +import { filterToUrlParams } from '@kne/react-filter'; -#### withFieldItem +const params = filterToUrlParams([ + { name: 'keyword', label: '关键词', value: { label: '测试', value: '测试' } }, + { name: 'city', label: '城市', value: [{ label: '上海', value: '010' }, { label: '北京', value: '020' }] }, +]); +// params.toString() => 'filterParams[keyword]=测试&filterParams[city]=上海:010,北京:020' -高阶组件,用于将表单字段组件包装为筛选项组件。 +// 自定义前缀 +filterToUrlParams(filterValue, { prefix: 'f' }); +// => 'f[keyword]=测试' -**函数签名:** -```javascript -withFieldItem(WrappedComponent): ReactComponent +// 无前缀(直接平铺到 URL) +filterToUrlParams(filterValue, { prefix: '' }); +// => 'keyword=测试' ``` -**参数说明:** +--- -| 参数名 | 说明 | 类型 | -|--------|------|------| -| WrappedComponent | 被包装的组件 | ReactComponent | - -**新增 Props:** +#### parseFilterEntry -| 属性名 | 说明 | 类型 | -|--------|------|------| -| name | 字段名 | string | -| label | 字段标签 | string | -| interceptor | 拦截器,用于值转换 | object | -| interceptor.input | 输入拦截器 | function | -| interceptor.output | 输出拦截器 | function | +解析 URL 参数中的单个筛选值项,反序列化为 `{ label, value }` 对象。 -#### pickSelectValues +| 参数 | 类型 | 说明 | +|-----|----------|---------------| +| str | `string` | URL 参数中的原始字符串 | -从筛选值中提取原始值数组。支持 `null`/`undefined`、原始值、`{ value }` 对象、`{ id }` 对象、以及它们的数组。 +**解析规则**: +- 无冒号:label 和 value 相同,如 `"测试"` → `{ label: '测试', value: '测试' }` +- 有冒号:冒号前为 label,冒号后为 value,如 `"启用:active"` → `{ label: '启用', value: 'active' }` -**函数签名:** ```javascript -pickSelectValues(value): string[] -``` +import { parseFilterEntry } from '@kne/react-filter'; -**参数说明:** - -| 参数名 | 说明 | 类型 | -|--------|------|------| -| value | 筛选值,支持多种格式 | any | +parseFilterEntry('测试'); +// => { label: '测试', value: '测试' } -**返回值:** +parseFilterEntry('启用:active'); +// => { label: '启用', value: 'active' } +``` -提取后的字符串值数组,空值会被过滤。 +--- -**示例:** -```javascript -pickSelectValues([{ value: 1 }, { id: 2 }, '3']) -// => ['1', '2', '3'] +#### takeFilterEntry -pickSelectValues({ value: 'open' }) -// => ['open'] +从 URL 参数中读取筛选值项,返回单选 `{ label, value }` 或多选数组。 -pickSelectValues(null) -// => [] -``` +| 参数 | 类型 | 默认值 | 说明 | +|-----------------|-------------------|-----------------|------------------| +| searchParams | `URLSearchParams` | - | URL 参数对象 | +| key | `string` | - | 参数名(不含前缀) | +| options.multi | `boolean` | `false` | 是否多选 | +| options.prefix | `string` | `'filterParams'` | URL 参数前缀 | -#### createFilterValueMapper +```javascript +import { takeFilterEntry } from '@kne/react-filter'; -声明式创建 `mapFilterValue` 函数。`Filter.getFilterValue` 默认只读取 `{ value }` 格式,而 SuperSelectFilterItem 等组件使用 `{ id, name }` 格式,需要额外处理。此工具通过声明字段映射规则,自动生成符合 `(filter, getFilterValue) => value` 签名的函数。 +// URL: ?filterParams[city]=上海:010,北京:020 +takeFilterEntry(searchParams, 'city', { multi: true }); +// => [{ label: '上海', value: '010' }, { label: '北京', value: '020' }] -**函数签名:** -```javascript -createFilterValueMapper(fieldMappers): function +takeFilterEntry(searchParams, 'keyword', { prefix: '' }); +// => { label: '测试', value: '测试' } ``` -**参数说明:** - -| 参数名 | 说明 | 类型 | 必填 | -|--------|------|------|------| -| fieldMappers | 字段名到映射规则的映射 | object | 是 | -| fieldMappers[fieldName] | 映射规则,支持字符串或函数 | string \| function | 是 | +--- -**映射规则类型:** +#### createUrlFilterReader -| 类型值 | 说明 | 输出格式 | -|--------|------|----------| -| `'string'` | 确保值为字符串类型 | `string` | -| `'multi'` | 多选,从 filter entry 提取值数组 | `string[]` | -| `'single'` | 单选,从 filter entry 提取第一个值 | `string` | -| `function` | 自定义转换函数,接收 `(rawValue, { entry, filter, value })` 返回新值 | any | +创建 URL 筛选参数读取器,自动追踪已消费的参数 key。配合 `useUrlFilter` 使用,readUrlParams 返回的 consumedKeys 可被自动清除。 -**返回值:** +| 参数 | 类型 | 默认值 | 说明 | +|-----------------|-------------------|-----------------|----------| +| searchParams | `URLSearchParams` | - | URL 参数对象 | +| options.prefix | `string` | `'filterParams'` | URL 参数前缀 | -返回一个 `mapFilterValue` 函数,签名为 `(filter, getFilterValue) => object`,可直接传给 BizUnit 等组件的 `mapFilterValue` 选项。 +**返回值**:`{ takeFilterEntry, getConsumedKeys }` -**示例:** ```javascript -const mapFilterValue = createFilterValueMapper({ - id: 'string', - roles: 'multi', - tenantOrgId: 'single', - status: (rawValue) => normalizeStatus(rawValue) -}); -const filterValue = mapFilterValue(filter, Filter.getFilterValue); -``` +import { createUrlFilterReader } from '@kne/react-filter'; -#### useUrlFilter +const { takeFilterEntry, getConsumedKeys } = createUrlFilterReader(searchParams); +const keyword = takeFilterEntry('keyword'); +const city = takeFilterEntry('city', { multi: true }); +// getConsumedKeys() => ['filterParams[keyword]', 'filterParams[city]'] +``` -从 URL 参数初始化 Filter 状态的 Hook。读取 URL 参数构建初始筛选值,并在挂载后自动清除已消费的 URL 参数。 +--- -**函数签名:** -```javascript -useUrlFilter(options): [array, function] -``` +#### useUrlFilter -**参数说明:** +从 URL 参数初始化 Filter 状态的 hook,读取 URL 参数构建初始筛选值,并在挂载后自动清除已消费的 URL 参数。 -| 参数名 | 说明 | 类型 | 必填 | -|--------|------|------|------| -| options | 配置对象 | object | 是 | -| options.readUrlParams | 读取 URL 参数的函数,返回包含 `consumedKeys` 的对象 | function | 是 | -| options.buildFilter | 根据 readUrlParams 返回值构建初始 filter 数组 | function | 是 | +> 需要 React Router 环境支持 `useSearchParams`。 -**返回值:** +| 参数 | 类型 | 说明 | +|---------------------|------------|----------------------------------------------| +| options.readUrlParams | `Function` | 读取 URL 参数并返回 `{ consumedKeys: string[], ...data }` | +| options.buildFilter | `Function` | 接收 readUrlParams 的返回值,构建初始 filter 数组 | -| 返回值 | 说明 | 类型 | -|--------|------|------| -| [0] | 初始筛选值数组 | array | -| [1] | 设置筛选值的函数 | function | +**返回值**:`[filter, setFilter]` -**示例:** ```javascript +import { useUrlFilter, createUrlParamsReader } from '@kne/react-filter'; + const [filter, setFilter] = useUrlFilter({ readUrlParams: (searchParams) => { const { take, getConsumedKeys } = createUrlParamsReader(searchParams); @@ -1958,278 +1364,200 @@ const [filter, setFilter] = useUrlFilter({ return { consumedKeys: getConsumedKeys(), orgId }; }, buildFilter: ({ orgId }) => [ + { name: 'status', value: { label: '开启', value: 'open' } }, ...(orgId ? [{ name: 'tenantOrgId', value: { label: orgId, value: orgId } }] : []) ] }); ``` -#### useUrlFilterValue +--- -从 URL 参数初始化 Filter 状态的 Hook(简化版)。基于 `useUrlFilter` 封装,使用 `createUrlFilterReader` 解析 `filterParams[key]` 格式的 URL 参数,自动解析 `label:value` 格式,支持单选和多选。 - -**函数签名:** -```javascript -useUrlFilterValue(mapping): [array, function] -``` - -**参数说明:** - -| 参数名 | 说明 | 类型 | 必填 | -|--------|------|------|------| -| mapping | URL 参数映射配置,支持数组或对象格式 | string[] \| object | 是 | - -**mapping 格式:** - -- **数组形式**:`['key1', 'key2']`,默认单选,自动创建 `{ name: key, value: { label, value } }` 格式的筛选项 -- **对象形式**:`{ key1: true, key2: { multi: true }, key3: fn }` - - 值为 `true`:单选,使用默认转换 - - 值为 `{ multi: true }`:多选,value 为 `[{ label, value }, ...]` 数组 - - 值为函数:自定义转换,接收解析后的值(单选为 `{ label, value }`,多选为数组),返回 filter 项或 `null`/falsy 跳过 +#### useUrlFilterValue -**返回值:** +`useUrlFilter` 的简化版,基于 `filterParams[key]` 格式自动解析 URL 参数,支持单选和多选。 -| 返回值 | 说明 | 类型 | -|--------|------|------| -| [0] | 初始筛选值数组 | array | -| [1] | 设置筛选值的函数 | function | +| 参数 | 类型 | 说明 | +|---------|------------------------|--------------------------------| +| mapping | `string[] \| Object` | URL 参数映射,支持数组、对象两种格式 | -**示例:** +**数组形式(默认单选):** ```javascript -// 数组形式(默认单选) +import { useUrlFilterValue } from '@kne/react-filter'; + const [filter, setFilter] = useUrlFilterValue(['keyword', 'status']); // URL: ?filterParams[keyword]=前端开发&filterParams[status]=招聘中:active // → filter: [ // { name: 'keyword', value: { label: '前端开发', value: '前端开发' } }, // { name: 'status', value: { label: '招聘中', value: 'active' } } // ] - -// 对象形式(多选 + 自定义转换) -const [filter, setFilter] = useUrlFilterValue({ - keyword: true, - city: { multi: true }, - status: (parsed) => parsed ? { name: 'status', value: parsed } : null -}); -// URL: ?filterParams[city]=上海:010,北京:020 -// → city 的 value: [{ label: '上海', value: '010' }, { label: '北京', value: '020' }] ``` -#### createUrlParamsReader - -创建 URL 参数读取器,自动追踪已消费的参数 key。 - -**函数签名:** +**对象形式(多选 + 自定义):** ```javascript -createUrlParamsReader(searchParams): { take, getConsumedKeys } +const [filter, setFilter] = useUrlFilterValue({ + keyword: true, // 单选 + city: { multi: true }, // 多选 + status: (parsed) => { // 自定义转换 + return parsed ? { name: 'status', value: parsed } : null; + } +}); +// URL: ?filterParams[keyword]=测试&filterParams[city]=上海:010,北京:020 ``` -**参数说明:** +--- -| 参数名 | 说明 | 类型 | 必填 | -|--------|------|------|------| -| searchParams | React Router 的 searchParams 对象 | URLSearchParams | 是 | - -**返回值:** +#### createUrlParamsReader -| 属性名 | 说明 | 类型 | -|--------|------|------| -| take | 读取指定 key 的值并标记为已消费 | function | -| getConsumedKeys | 获取所有已消费的 key 列表 | function | +创建通用 URL 参数读取器,自动追踪已消费的参数 key。 -#### stripConsumedUrlParams +| 参数 | 类型 | 说明 | +|--------------|-------------------|----------| +| searchParams | `URLSearchParams` | URL 参数对象 | -从 URL 参数中移除已消费的 key,返回新的 URLSearchParams。无变化时返回 `null`。 +**返回值**:`{ take, getConsumedKeys }` +- `take(key)` - 读取参数值,记录已消费 +- `getConsumedKeys()` - 返回已消费的 key 列表 -**函数签名:** ```javascript -stripConsumedUrlParams(searchParams, consumedKeys): URLSearchParams | null -``` - -**参数说明:** - -| 参数名 | 说明 | 类型 | 必填 | -|--------|------|------|------| -| searchParams | 当前 URL 参数 | URLSearchParams | 是 | -| consumedKeys | 需要移除的 key 列表 | string[] | 是 | - -**返回值:** - -移除后的新 URLSearchParams 对象,无变化返回 `null`。 - -#### filterToUrlParams +import { createUrlParamsReader } from '@kne/react-filter'; -将筛选值数组序列化为 URLSearchParams,保留 label 信息以便反序列化还原完整筛选状态。参数以 `prefix[key]` 格式存入 URL,避免与其他查询参数冲突。 - -**序列化格式**(使用冒号分隔 label 和 value,逗号分隔多值): -- 单值且 label === value:`prefix[name]=value`(如输入框) -- 单值且 label !== value:`prefix[name]=label:value` -- 多值:`prefix[name]=label1:value1,label2:value2` - -**函数签名:** -```javascript -filterToUrlParams(filterValue, options?): URLSearchParams +const { take, getConsumedKeys } = createUrlParamsReader(searchParams); +const orgId = take('tenantOrgId'); +const orgName = take('orgName'); +// getConsumedKeys() => ['tenantOrgId', 'orgName'] ``` -**参数说明:** +--- -| 参数名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| filterValue | 筛选值数组,格式为 `[{ name, label, value }, ...]` | array | 是 | - | -| options.prefix | URL 参数前缀 | string | 否 | `'filterParams'` | +#### stripConsumedUrlParams -**示例:** -```javascript -const params = filterToUrlParams([ - { name: 'keyword', label: '关键词', value: { label: '测试', value: '测试' } }, - { name: 'city', label: '城市', value: [{ label: '上海', value: '010' }, { label: '北京', value: '020' }] }, - { name: 'status', label: '状态', value: { label: '启用', value: 'active' } }, -]); -// params.toString() => 'filterParams[keyword]=测试&filterParams[city]=上海:010,北京:020&filterParams[status]=启用:active' -``` +从 URL 参数中移除已消费的 key,返回新的 URLSearchParams 或 null(无变化时)。 -#### createUrlFilterReader +| 参数 | 类型 | 说明 | +|--------------|-------------------|-------------------| +| searchParams | `URLSearchParams` | 当前 URL 参数 | +| consumedKeys | `string[]` | 需要移除的 key 列表 | -创建 URL 筛选参数读取器,自动追踪已消费的参数 key。配合 `useUrlFilter` 使用,读取后返回的 `consumedKeys` 可被自动从 URL 中清除。 +**返回值**:`URLSearchParams | null` -**函数签名:** ```javascript -createUrlFilterReader(searchParams, options?): { takeFilterEntry, getConsumedKeys } -``` - -**参数说明:** +import { stripConsumedUrlParams } from '@kne/react-filter'; -| 参数名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| searchParams | React Router 的 searchParams 对象 | URLSearchParams | 是 | - | -| options.prefix | URL 参数前缀,需与 `filterToUrlParams` 使用的前缀一致 | string | 否 | `'filterParams'` | +const nextParams = stripConsumedUrlParams(searchParams, ['tenantOrgId', 'orgName']); +if (nextParams) { + setSearchParams(nextParams, { replace: true }); +} +``` -**返回值:** +--- -| 属性名 | 说明 | 类型 | -|--------|------|------| -| takeFilterEntry | 读取指定 key 的筛选值并标记为已消费 | function | -| getConsumedKeys | 获取所有已消费的 key 列表(含前缀) | function | +### 拦截器 -**takeFilterEntry 签名:** -```javascript -takeFilterEntry(key, options?): { label: string, value: string } | { label: string, value: string }[] | null -``` +用于 SuperSelect 组件的 `{id, name}` 与 Filter 的 `{label, value}` 数据格式互转。 -| 参数名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| key | 参数名(不含前缀) | string | 是 | - | -| options.multi | 是否多选,多选返回数组 | boolean | 否 | false | +#### singleSelectInterceptor -**示例:** -```javascript -const [filter, setFilter] = useUrlFilter({ - readUrlParams: (searchParams) => { - const { takeFilterEntry, getConsumedKeys } = createUrlFilterReader(searchParams); - const keyword = takeFilterEntry('keyword'); - const city = takeFilterEntry('city', { multi: true }); - return { consumedKeys: getConsumedKeys(), keyword, city }; - }, - buildFilter: ({ keyword, city }) => [ - ...(keyword ? [{ name: 'keyword', label: '关键词', value: keyword }] : []), - ...(city ? [{ name: 'city', label: '城市', value: city }] : []), - ], -}); -``` +单选拦截器:`{id, name}` ↔ `{label, value}`。 -#### parseFilterEntry +| 属性 | 类型 | 说明 | +|---------|------------|---------------------------------------| +| input | `Function` | `{id, name}` → `{label, value}` 的转换 | +| output | `Function` | `{label, value}` → `{id, name}` 的转换 | -解析 URL 参数中的单个筛选值项,反序列化为 `{ label, value }` 对象。 +#### multiSelectInterceptor -**解析规则:** -- 无冒号:label 和 value 相同,如 `"测试"` → `{ label: '测试', value: '测试' }` -- 有冒号:冒号前为 label,冒号后为 value,如 `"启用:active"` → `{ label: '启用', value: 'active' }` +多选拦截器:`[{id, name}]` ↔ `[{label, value}]`。 -**函数签名:** -```javascript -parseFilterEntry(str): { label: string, value: string } -``` +| 属性 | 类型 | 说明 | +|---------|------------|-------------------------------------------| +| input | `Function` | `[{id, name}]` → `[{label, value}]` 的转换 | +| output | `Function` | `[{label, value}]` → `[{id, name}]` 的转换 | -**参数说明:** +#### filterInterceptors -| 参数名 | 说明 | 类型 | 必填 | -|--------|------|------|------| -| str | URL 参数中的原始字符串 | string | 是 | +拦截器集合对象。 -**示例:** ```javascript -parseFilterEntry('测试') -// => { label: '测试', value: '测试' } +import { filterInterceptors, singleSelectInterceptor, multiSelectInterceptor } from '@kne/react-filter'; -parseFilterEntry('启用:active') -// => { label: '启用', value: 'active' } +// 两种引用方式等价 +filterInterceptors.single === singleSelectInterceptor; // true +filterInterceptors.multi === multiSelectInterceptor; // true ``` -#### takeFilterEntry - -从 URL 参数中读取筛选值项(低级 API)。推荐使用 `createUrlFilterReader` 代替,可自动追踪已消费的 key。 +**使用示例:** -**函数签名:** ```javascript -takeFilterEntry(searchParams, key, options?): { label: string, value: string } | { label: string, value: string }[] | null +import { filterInterceptors } from '@kne/react-filter'; + +// 在 SuperSelect 组件中使用单选拦截 + + +// 多选拦截 + ``` -**参数说明:** +--- -| 参数名 | 说明 | 类型 | 必填 | 默认值 | -|--------|------|------|------|--------| -| searchParams | URL 参数对象 | URLSearchParams | 是 | - | -| key | 参数名(不含前缀) | string | 是 | - | -| options.multi | 是否多选,多选返回数组 | boolean | 否 | false | -| options.prefix | URL 参数前缀 | string | 否 | `'filterParams'` | +### 工具方法 -**示例:** -```javascript -// URL: ?filterParams[city]=上海:010,北京:020 -takeFilterEntry(searchParams, 'city', { multi: true }) -// => [{ label: '上海', value: '010' }, { label: '北京', value: '020' }] -``` +#### pickSelectValues -#### filterInterceptors +从筛选值中提取原始值数组。支持原始值、`{ value }` 对象、`{ id }` 对象以及它们的数组。 -预设拦截器集合,提供用于 SuperSelect 系列组件的值格式转换拦截器。SuperSelect 等组件使用 `{ id, name }` 格式,而 Filter 上下文使用 `{ label, value }` 格式,需要通过拦截器进行自动转换。 +| 参数 | 类型 | 说明 | +|-------|-------|-------------| +| value | `any` | 筛选值,支持多种格式 | -**导出成员:** +```javascript +import { pickSelectValues } from '@kne/react-filter'; -|| 名称 | 说明 | -||------|------| -|| filterInterceptors | 拦截器集合对象,包含 `single` 和 `multi` 两个拦截器 | -|| singleSelectInterceptor | 单选拦截器,用于 valueKey="id" labelKey="name" 的单选场景 | -|| multiSelectInterceptor | 多选拦截器,用于 valueKey="id" labelKey="name" 的多选场景 | +pickSelectValues([{ value: 1 }, { id: 2 }, '3']); +// => ['1', '2', '3'] -**filterInterceptors 结构:** +pickSelectValues({ value: 'open' }); +// => ['open'] -|| 属性名 | 说明 | 类型 | -||--------|------|------| -|| single | 单选拦截器,等价于 singleSelectInterceptor | object | -|| multi | 多选拦截器,等价于 multiSelectInterceptor | object | +pickSelectValues(null); +// => [] +``` -**拦截器结构:** +#### createFilterValueMapper -每个拦截器包含 `input` 和 `output` 两个函数: +声明式创建 mapFilterValue 函数。`Filter.getFilterValue` 默认只读取 `{ value }`,而 SuperSelectFilterItem 等组件使用 `{ id, name }` 格式,需要额外处理。此工具通过声明字段映射规则,自动生成转换函数。 -|| 属性名 | 说明 | 转换方向 | -||--------|------|----------| -|| input | 输入拦截器 | 将上下文值 `{ label, value }` 转为组件内部格式 `{ id, name }` | -|| output | 输出拦截器 | 将组件内部值 `{ id, name }` 转回上下文格式 `{ label, value }` | +| 参数 | 类型 | 说明 | +|--------------|----------|-------------------| +| fieldMappers | `Object` | 字段名到映射规则的映射 | -**使用方式:** +**映射规则类型:** -配合 `SuperSelectFilterItem` 的 `interceptor` 属性使用: +| 规则 | 说明 | +|-----------|----------------------------------------| +| `'string'` | 确保值为字符串类型 | +| `'multi'` | 多选,从 filter entry 提取值数组 | +| `'single'` | 单选,从 filter entry 提取第一个值 | +| `Function` | 自定义转换,接收 `(rawValue, { entry, filter, value })` | ```javascript -import { SuperSelectFilterItem, singleSelectInterceptor, multiSelectInterceptor } from '@components/Filter'; - -// 单选场景 - +import { createFilterValueMapper } from '@kne/react-filter'; -// 多选场景 - +const mapFilterValue = createFilterValueMapper({ + id: 'string', + roles: 'multi', + tenantOrgId: 'single', + status: (rawValue) => normalizeStatus(rawValue) +}); -// 或通过 filterInterceptors 解构 -const { single, multi } = filterInterceptors; - +const filterValue = mapFilterValue(filter, Filter.getFilterValue); ``` diff --git a/src/components/Filter/SearchInput.js b/src/components/Filter/SearchInput.js deleted file mode 100644 index c60588f2..00000000 --- a/src/components/Filter/SearchInput.js +++ /dev/null @@ -1,19 +0,0 @@ -import {SearchInput as SearchInputField} from "@components/Common"; -import withFilterValue from "./withFilterValue"; -import {useIntl} from "@components/Intl"; - -const SearchInput = withFilterValue(({label, onChange, value, placeholder, ...props}) => { - const {formatMessage} = useIntl({moduleName: 'Filter'}); - return ( - { - onChange({label: value, value}); - }} - /> - ); -}); - -export default SearchInput; diff --git a/src/components/Filter/context.js b/src/components/Filter/context.js deleted file mode 100644 index 1a0e2a13..00000000 --- a/src/components/Filter/context.js +++ /dev/null @@ -1,7 +0,0 @@ -import { createContext, useContext as useReactContext } from "react"; - -export const context = createContext({}); - -export const { Provider } = context; - -export const useContext = () => useReactContext(context); diff --git a/src/components/Filter/createFilterValueMapper.js b/src/components/Filter/createFilterValueMapper.js deleted file mode 100644 index 7198ba8f..00000000 --- a/src/components/Filter/createFilterValueMapper.js +++ /dev/null @@ -1,86 +0,0 @@ -import pickSelectValues from './pickSelectValues'; - -/** - * 声明式创建 mapFilterValue 函数。 - * - * Filter.getFilterValue 默认只读取 { value },而 SuperSelectFilterItem 等 - * 组件使用 { id, name } 格式,需要额外处理。此工具通过声明字段映射规则, - * 自动生成符合 (filter, getFilterValue) => value 签名的函数。 - * - * @param {Object} fieldMappers - 字段名到映射规则的映射 - * @param {'string'} fieldMappers[field] - 确保值为字符串类型 - * @param {'multi'} fieldMappers[field] - 多选,从 filter entry 提取值数组 - * @param {'single'} fieldMappers[field] - 单选,从 filter entry 提取第一个值 - * @param {Function} fieldMappers[field] - 自定义转换,接收 (rawValue, { entry, filter, value }) 返回新值 - * - * @example - * const mapFilterValue = createFilterValueMapper({ - * id: 'string', - * roles: 'multi', - * tenantOrgId: 'single', - * status: (rawValue) => normalizeStatus(rawValue) - * }); - * - * // 使用方式与手写 mapFilterValue 一致 - * const filterValue = mapFilterValue(filter, Filter.getFilterValue); - * - * // 也可直接传给 BizUnit 的 options.mapFilterValue - * - */ -const createFilterValueMapper = (fieldMappers) => { - return (filter, getFilterValue) => { - const value = getFilterValue(filter); - - Object.entries(fieldMappers).forEach(([fieldName, mapper]) => { - const entry = filter.find(item => item.name === fieldName); - - if (mapper === 'string') { - if (value[fieldName] != null && value[fieldName] !== '') { - value[fieldName] = String(value[fieldName]); - } - } else if (mapper === 'multi') { - if (entry) { - const values = pickSelectValues(entry.value); - if (values.length) { - value[fieldName] = values; - } else { - delete value[fieldName]; - } - } - } else if (mapper === 'single') { - if (entry) { - const values = pickSelectValues(entry.value); - if (values[0] != null) { - value[fieldName] = values[0]; - } else { - delete value[fieldName]; - } - } else if (value[fieldName] != null && typeof value[fieldName] === 'object') { - const values = pickSelectValues(value[fieldName]); - if (values.length) { - value[fieldName] = values[0]; - } else { - delete value[fieldName]; - } - } - } else if (typeof mapper === 'function') { - let rawValue = value[fieldName]; - if (rawValue != null && typeof rawValue === 'object' && 'value' in rawValue) { - rawValue = rawValue.value; - } - if (rawValue != null && rawValue !== '') { - const result = mapper(rawValue, { entry, filter, value }); - if (result != null && result !== '') { - value[fieldName] = result; - } else { - delete value[fieldName]; - } - } - } - }); - - return value; - }; -}; - -export default createFilterValueMapper; diff --git a/src/components/Filter/doc/example.json b/src/components/Filter/doc/example.json index 5a18f45a..85cdfdd2 100644 --- a/src/components/Filter/doc/example.json +++ b/src/components/Filter/doc/example.json @@ -1,173 +1,3 @@ { - "isFull": true, - "list": [ - { - "title": "基础用法", - "description": "展示 Filter 组件的基本使用方式,包括各种常见的筛选字段类型", - "code": "./base.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - } - ] - }, - { - "title": "展示和Enum一起使用", - "description": "", - "code": "./use-enum.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - }, - { - "name": "_Enum", - "packageName": "@components/Enum" - } - ] - }, - { - "title": "高级筛选", - "description": "展示 AdvancedFilter 组件的高级筛选功能,适用于复杂筛选场景", - "code": "./advanced-filter.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - } - ] - }, - { - "title": "树形筛选", - "description": "展示 TreeFilterItem 树形选择组件的使用", - "code": "./tree-item.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - }, - { - "name": "antd", - "packageName": "antd" - }, - { - "name": "_data", - "packageName": "@components/Filter/doc/mock/tree-data.json" - } - ] - }, - { - "title": "筛选值展示", - "description": "展示 FilterValueDisplay、FilterItem、FilterLines、PopoverItem 等组件的独立使用", - "code": "./filter-value-display.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - }, - { - "name": "antd", - "packageName": "antd" - } - ] - }, - { - "title": "数值范围筛选", - "description": "展示 NumberRangeFilterItem 数值范围筛选组件的使用", - "code": "./number-range.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - } - ] - }, - { - "title": "级联筛选", - "description": "展示 CascaderFilterItem 级联选择组件的使用", - "code": "./cascader.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - } - ] - }, - { - "title": "自定义筛选字段", - "description": "展示如何使用 withFilterValue 将原生 Select 组件包装成筛选字段", - "code": "./custom-fields.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - }, - { - "name": "antd", - "packageName": "antd" - } - ] - }, - { - "title": "FilterProvider 和 useFilter", - "description": "展示如何使用 FilterProvider 和 useFilter Hook 自定义筛选界面", - "code": "./filter-provider.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - }, - { - "name": "antd", - "packageName": "antd" - } - ] - }, - { - "title": "筛选值映射与提取", - "description": "展示 createFilterValueMapper 声明式值映射和 pickSelectValues 值提取工具的用法", - "code": "./filter-value-mapper.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - }, - { - "name": "antd", - "packageName": "antd" - } - ] - }, - { - "title": "URL 筛选参数", - "description": "展示 filterToUrlParams、parseFilterEntry、takeFilterEntry、createUrlFilterReader 等 URL 参数序列化与反序列化工具的用法", - "code": "./use-url-filter.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - }, - { - "name": "antd", - "packageName": "antd" - } - ] - }, - { - "title": "筛选值拦截器", - "description": "展示 filterInterceptors、singleSelectInterceptor、multiSelectInterceptor 拦截器的用法,解决 SuperSelect 组件 { id, name } 与 Filter 上下文 { label, value } 格式不匹配的问题", - "code": "./filter-interceptors.js", - "scope": [ - { - "name": "_Filter", - "packageName": "@components/Filter" - }, - { - "name": "antd", - "packageName": "antd" - } - ] - } - ] + "reference": "@kne/react-filter" } diff --git a/src/components/Filter/fields/DatePickerFilterItem.js b/src/components/Filter/fields/DatePickerFilterItem.js deleted file mode 100644 index 8a4a6bd4..00000000 --- a/src/components/Filter/fields/DatePickerFilterItem.js +++ /dev/null @@ -1,21 +0,0 @@ -import {DatePicker} from "antd"; -import withFieldItem from "../withFieldItem"; -import dayjs from "dayjs"; - -const DatePickerFilterItem = withFieldItem(({value, onChange, picker = "date", ...props}) => { - return ( { - const {format} = Object.assign({format: "YYYY-MM-DD"}, props); - value && onChange({ - label: picker !== "date" ? `${value.startOf(picker).format(format)}~${value - .endOf(picker) - .format(format)}` : value.format(format), value: new Date(value.startOf(picker).valueOf()), - }); - }} - />); -}); - -export default DatePickerFilterItem; diff --git a/src/components/Filter/fields/DateRangePickerFilterItem.js b/src/components/Filter/fields/DateRangePickerFilterItem.js deleted file mode 100644 index e572ed1b..00000000 --- a/src/components/Filter/fields/DateRangePickerFilterItem.js +++ /dev/null @@ -1,29 +0,0 @@ -import {DatePicker, Flex} from "antd"; -import withFieldItem from "../withFieldItem"; -import dayjs from "dayjs"; -import style from "../style.module.scss"; - -const DateRangePickerFilterItem = withFieldItem(({value, onChange, header, ...props}) => { - return ( - {typeof header === "function" ? header({value, onChange}) : header} - dayjs(item))} - onChange={(value) => { - const {format} = Object.assign({format: "YYYY-MM-DD"}, props); - onChange({ - label: value.map((item) => item.format(format)).join("~"), - value: value.map((item) => new Date(item.valueOf())), - }); - }} - /> - ); -}); - -export default DateRangePickerFilterItem; diff --git a/src/components/Filter/fields/InputFilterItem.js b/src/components/Filter/fields/InputFilterItem.js deleted file mode 100644 index 3e355856..00000000 --- a/src/components/Filter/fields/InputFilterItem.js +++ /dev/null @@ -1,43 +0,0 @@ -import PopoverItem from "../PopoverItem"; -import { Input } from "antd"; -import get from "lodash/get"; -import style from "../style.module.scss"; - -const InputFilterItem = ({ - label, - value, - onChange, - placeholder, - onValidate, - overlayClassName, - placement, - onOpenChange, - ...props -}) => { - return ( - - {({ value, onChange }) => ( - - onChange( - e.target.value - ? { label: e.target.value, value: e.target.value } - : null - ) - } - /> - )} - - ); -}; - -export default InputFilterItem; diff --git a/src/components/Filter/fields/NumberRangeFilterItem.js b/src/components/Filter/fields/NumberRangeFilterItem.js deleted file mode 100644 index 6929d305..00000000 --- a/src/components/Filter/fields/NumberRangeFilterItem.js +++ /dev/null @@ -1,90 +0,0 @@ -import PopoverItem from "../PopoverItem"; -import {Input, InputNumber, Space} from "antd"; -import get from "lodash/get"; -import isNumber from "lodash/isNumber"; -import {useIntl} from "@components/Intl"; -import style from "../style.module.scss"; -import React from "react"; - -const computedFilterValue = (range, unit, formatMessage) => { - if (!isNumber(range[0]) && !isNumber(range[1])) { - return null; - } - - return { - label: ((range) => { - if (isNumber(range[0]) && isNumber(range[1])) { - return `${range[0]}-${range[1]}${unit || ""}`; - } - if (isNumber(range[0])) { - return formatMessage({id: "over"}, {count: range[0], unit}); - } - if (isNumber(range[1])) { - return formatMessage({id: "lessThan"}, {count: range[1], unit}); - } - })(range), value: [range[0] || null, range[1] || null], - }; -}; - -const defaultPropsOnValidate = (value) => { - const range = get(value, "value"); - return !(range && isNumber(range[0]) && isNumber(range[1]) && range[1] < range[0]); -}; - -const InputFilterItem = ({ - label, - value, - onChange, - placeholder, - onValidate = defaultPropsOnValidate, - overlayClassName, - placement, - onOpenChange, - unit, - ...props - }) => { - const {formatMessage} = useIntl({moduleName: "Filter"}); - return ( - {({value, onChange}) => ( - { - onChange(computedFilterValue([target, get(value, "value[1]")], unit, formatMessage)); - }} - /> - - { - onChange(computedFilterValue([get(value, "value[0]"), target], unit, formatMessage)); - }} - /> - {unit && ()} - )} - ); -}; - -export default InputFilterItem; diff --git a/src/components/Filter/fields/TypeDateRangePickerFilterItem.js b/src/components/Filter/fields/TypeDateRangePickerFilterItem.js deleted file mode 100644 index 29fda9ee..00000000 --- a/src/components/Filter/fields/TypeDateRangePickerFilterItem.js +++ /dev/null @@ -1,57 +0,0 @@ -import PopoverItem from "../PopoverItem"; -import TypeDateRangePickerField from "@common/components/TypeDateRangePickerField"; -import get from "lodash/get"; -import style from "../style.module.scss"; -import dayjs from "dayjs"; - -const TypeDateRangePickerFilterItem = ({ - label, - value, - onChange, - ...props -}) => { - return ( - { - const value = item?.value; - return ( - value?.type && Array.isArray(value?.value) && value.value.length === 2 - ); - }} - > - {({ value, onChange }) => ( - { - const { format } = Object.assign({ format: "YYYY-MM-DD" }, props); - const value = pickerValue?.value || []; - onChange({ - label: (() => { - if (value[0] && !value[1]) { - return `${dayjs(value[0]).format(format)}以后`; - } - if (!value[0] && value[1]) { - return `${dayjs(value[1]).format(format)}以前`; - } - if (value[0] && value[1]) { - return `${dayjs(value[0]).format(format)}~${dayjs( - value[1] - ).format(format)}`; - } - return ""; - })(), - value: pickerValue, - }); - }} - /> - )} - - ); -}; - -export default TypeDateRangePickerFilterItem; diff --git a/src/components/Filter/fields/index.js b/src/components/Filter/fields/index.js deleted file mode 100644 index caa98c21..00000000 --- a/src/components/Filter/fields/index.js +++ /dev/null @@ -1,49 +0,0 @@ -import {FormattedMessage} from "@components/Intl"; -import withFieldItem from "../withFieldItem"; -import AdvancedSelectField, { - UserField, -} from "@common/components/AdvancedSelectField"; -import SuperSelectField, { - SuperSelectTableListField, SuperSelectUserField, -} from "@common/components/SuperSelectField"; -import FunctionSelectField from "@common/components/FunctionSelectField"; -import AddressSelectField from "@common/components/AddressSelectField"; -import IndustrySelectField from "@common/components/IndustrySelectField"; -import CascaderField from "@common/components/CascaderField"; -import TreeField from "@common/components/TreeField"; -import InputFilterItemField from "./InputFilterItem"; -import NumberRangeFilterItemField from "./NumberRangeFilterItem"; - -const withInputDefaultPlaceholder = (WrappedComponent) => ({placeholder, label, ...props}) => ( - {(text) => { - return (); - }} -); - -export const AdvancedSelectFilterItem = withFieldItem(AdvancedSelectField); -export const SuperSelectFilterItem = withFieldItem(SuperSelectField); -export const SuperSelectTableListFilterItem = withFieldItem(SuperSelectTableListField); -export const SuperSelectUserFilterItem = withFieldItem(SuperSelectUserField); -export const UserFilterItem = withFieldItem(UserField); -export const FunctionSelectFilterItem = withFieldItem(FunctionSelectField); -export const IndustrySelectFilterItem = withFieldItem(IndustrySelectField); - -export const CityFilterItem = withFieldItem(AddressSelectField); - -export const CascaderFilterItem = withFieldItem(CascaderField); - -export const TreeFilterItem = withFieldItem(TreeField); -export const InputFilterItem = withInputDefaultPlaceholder(InputFilterItemField); -export const NumberRangeFilterItem = withInputDefaultPlaceholder(NumberRangeFilterItemField); - -export {default as DatePickerFilterItem} from "./DatePickerFilterItem"; -export {default as DateRangePickerFilterItem} from "./DateRangePickerFilterItem"; -export {default as TypeDateRangePickerFilterItem} from "./TypeDateRangePickerFilterItem"; diff --git a/src/components/Filter/filterInterceptors.js b/src/components/Filter/filterInterceptors.js deleted file mode 100644 index 594264a8..00000000 --- a/src/components/Filter/filterInterceptors.js +++ /dev/null @@ -1,67 +0,0 @@ -const toIdName = item => { - if (!item) { - return null; - } - if (item.id != null) { - return item; - } - if (item.value != null) { - return {id: item.value, name: item.label}; - } - return item; -}; - -const toLabelValue = item => { - if (!item) { - return null; - } - return { - label: item.name ?? item.label, - value: item.id ?? item.value - }; -}; - -/** - * 单选 SuperSelect interceptor:{id, name} ↔ {label, value} - * 适用于 valueKey="id" labelKey="name" 的单选场景 - */ -export const singleSelectInterceptor = { - input: value => { - if (!value) { - return value; - } - const item = Array.isArray(value) ? value[0] : value; - const result = toIdName(item); - return result; - }, - output: selected => { - if (!selected) { - return selected; - } - const item = Array.isArray(selected) ? selected[0] : selected; - return toLabelValue(item); - } -}; - -/** - * 多选 SuperSelect interceptor:[{id, name}] ↔ [{label, value}] - * 适用于 valueKey="id" labelKey="name" 的多选场景 - */ -export const multiSelectInterceptor = { - input: value => { - if (!value) { - return value; - } - const list = Array.isArray(value) ? value : [value]; - return list.map(toIdName).filter(Boolean); - }, - output: selected => { - if (!selected) { - return selected; - } - const list = Array.isArray(selected) ? selected : [selected]; - return list.map(toLabelValue).filter(Boolean); - } -}; - -export default {single: singleSelectInterceptor, multi: multiSelectInterceptor}; diff --git a/src/components/Filter/filterToUrlParams.js b/src/components/Filter/filterToUrlParams.js deleted file mode 100644 index a09f2e5a..00000000 --- a/src/components/Filter/filterToUrlParams.js +++ /dev/null @@ -1,192 +0,0 @@ -/** - * 将筛选值数组序列化为 URLSearchParams,保留 label 信息以便反序列化还原完整筛选状态。 - * - * 序列化格式(使用冒号分隔 label 和 value,逗号分隔多值): - * - 单值且 label === value:prefix[name]=value(如输入框) - * - 单值且 label !== value:prefix[name]=label:value - * - 多值:prefix[name]=label1:value1,label2:value2 - * - * @param {Array} filterValue - 筛选值数组,格式为 [{ name, label, value }, ...] - * @param {Object} [options] - 选项 - * @param {string} [options.prefix='filterParams'] - URL 参数前缀,设为空字符串则不加前缀 - * @returns {URLSearchParams} - * - * @example - * const params = filterToUrlParams([ - * { name: 'keyword', label: '关键词', value: { label: '测试', value: '测试' } }, - * { name: 'city', label: '城市', value: [{ label: '上海', value: '010' }, { label: '北京', value: '020' }] }, - * { name: 'status', label: '状态', value: { label: '启用', value: 'active' } }, - * ]); - * // params.toString() => 'filterParams[keyword]=测试&filterParams[city]=上海:010,北京:020&filterParams[status]=启用:active' - * - * // 自定义前缀 - * filterToUrlParams(filterValue, { prefix: 'f' }) - * // => 'f[keyword]=测试&f[city]=上海:010,北京:020' - * - * // 无前缀(直接平铺) - * filterToUrlParams(filterValue, { prefix: '' }) - * // => 'keyword=测试&city=上海:010,北京:020' - */ -const filterToUrlParams = (filterValue, { prefix = 'filterParams' } = {}) => { - const params = new URLSearchParams(); - - if (!Array.isArray(filterValue)) return params; - - filterValue.forEach(({ name, value }) => { - if (value == null || value === '') return; - - const key = prefix ? `${prefix}[${name}]` : name; - - if (Array.isArray(value)) { - if (value.length === 0) return; - const serialized = value - .map((item) => serializeEntry(item)) - .filter(Boolean) - .join(','); - if (serialized) params.set(key, serialized); - } else { - const serialized = serializeEntry(value); - if (serialized) params.set(key, serialized); - } - }); - - return params; -}; - -/** - * 序列化单个筛选值项。 - * - label === value 时返回 value(简化格式) - * - 否则返回 label:value - */ -const serializeEntry = (item) => { - if (item == null) return null; - if (typeof item !== 'object') return String(item); - - const label = item.label ?? item.name; - const value = item.value ?? item.id; - - if (value == null || value === '') return null; - if (label === value) return String(value); - return `${label}:${value}`; -}; - -/** - * 解析 URL 参数中的单个筛选值项,反序列化为 { label, value } 对象。 - * 与 filterToUrlParams 配合使用。 - * - * 解析规则: - * - 无冒号:label 和 value 相同,如 "测试" → { label: '测试', value: '测试' } - * - 有冒号:冒号前为 label,冒号后为 value,如 "启用:active" → { label: '启用', value: 'active' } - * - * @param {string} str - URL 参数中的原始字符串 - * @returns {{ label: string, value: string }} - * - * @example - * parseFilterEntry('测试') - * // => { label: '测试', value: '测试' } - * - * parseFilterEntry('启用:active') - * // => { label: '启用', value: 'active' } - */ -const parseFilterEntry = (str) => { - const colonIndex = str.indexOf(':'); - if (colonIndex === -1) { - return { label: str, value: str }; - } - return { - label: str.slice(0, colonIndex), - value: str.slice(colonIndex + 1), - }; -}; - -/** - * 从 URL 参数中读取筛选值项,返回单选 { label, value } 或多选数组。 - * - * @param {URLSearchParams} searchParams - URL 参数对象 - * @param {string} key - 参数名(不含前缀) - * @param {Object} [options] - 选项 - * @param {boolean} [options.multi=false] - 是否多选,多选返回数组 - * @param {string} [options.prefix='filterParams'] - URL 参数前缀,设为空字符串则不加前缀 - * @returns {{ label: string, value: string } | { label: string, value: string }[] | null} - * - * @example - * // URL: ?filterParams[city]=上海:010,北京:020&filterParams[status]=启用:active - * takeFilterEntry(searchParams, 'city', { multi: true }) - * // => [{ label: '上海', value: '010' }, { label: '北京', value: '020' }] - * - * // 无前缀 - * takeFilterEntry(searchParams, 'keyword', { prefix: '' }) - * // => { label: '测试', value: '测试' } - */ -const takeFilterEntry = (searchParams, key, { multi = false, prefix = 'filterParams' } = {}) => { - const fullKey = prefix ? `${prefix}[${key}]` : key; - if (!searchParams.has(fullKey)) return null; - - const raw = searchParams.get(fullKey); - if (!raw) return null; - - if (multi) { - return raw.split(',').map(parseFilterEntry); - } - - // 单选时,如果包含逗号,只取第一个 - const firstEntry = raw.includes(',') ? raw.split(',')[0] : raw; - return parseFilterEntry(firstEntry); -}; - -/** - * 创建 URL 筛选参数读取器,自动追踪已消费的参数 key。 - * 配合 useUrlFilter 使用,readUrlParams 返回的 consumedKeys 可被自动清除。 - * - * @param {URLSearchParams} searchParams - React Router 的 searchParams 对象 - * @param {Object} [options] - 选项 - * @param {string} [options.prefix='filterParams'] - URL 参数前缀 - * @returns {{ takeFilterEntry, getConsumedKeys }} - * - * @example - * const { takeFilterEntry, getConsumedKeys } = createUrlFilterReader(searchParams); - * const keyword = takeFilterEntry('keyword'); - * const city = takeFilterEntry('city', { multi: true }); - * const status = takeFilterEntry('status'); - * // getConsumedKeys() => ['filterParams[keyword]', 'filterParams[city]', 'filterParams[status]'] - * - * // 配合 useUrlFilter 使用 - * const [filter, setFilter] = useUrlFilter({ - * readUrlParams: (searchParams) => { - * const { takeFilterEntry, getConsumedKeys } = createUrlFilterReader(searchParams); - * const keyword = takeFilterEntry('keyword'); - * const city = takeFilterEntry('city', { multi: true }); - * return { consumedKeys: getConsumedKeys(), keyword, city }; - * }, - * buildFilter: ({ keyword, city }) => [ - * ...(keyword ? [{ name: 'keyword', label: '关键词', value: keyword }] : []), - * ...(city ? [{ name: 'city', label: '城市', value: city }] : []), - * ], - * }); - */ -const createUrlFilterReader = (searchParams, { prefix = 'filterParams' } = {}) => { - const consumedKeys = []; - - const takeFilterEntry = (key, { multi = false } = {}) => { - const fullKey = prefix ? `${prefix}[${key}]` : key; - if (!searchParams.has(fullKey)) return null; - - consumedKeys.push(fullKey); - const raw = searchParams.get(fullKey); - if (!raw) return null; - - if (multi) { - return raw.split(',').map(parseFilterEntry); - } - - const firstEntry = raw.includes(',') ? raw.split(',')[0] : raw; - return parseFilterEntry(firstEntry); - }; - - const getConsumedKeys = () => consumedKeys; - - return { takeFilterEntry, getConsumedKeys }; -}; - -export default filterToUrlParams; -export { parseFilterEntry, takeFilterEntry, createUrlFilterReader }; diff --git a/src/components/Filter/getFilterValue.js b/src/components/Filter/getFilterValue.js deleted file mode 100644 index 06aab17f..00000000 --- a/src/components/Filter/getFilterValue.js +++ /dev/null @@ -1,15 +0,0 @@ -import transform from "lodash/transform"; - -const getFilterValue = (filterValue) => { - return transform( - filterValue, - (result, { name, value }) => { - result[name] = Array.isArray(value) - ? value.map(({ value }) => value) - : value?.value; - }, - {} - ); -}; - -export default getFilterValue; diff --git a/src/components/Filter/index.js b/src/components/Filter/index.js index 6ee292bf..eff270d4 100644 --- a/src/components/Filter/index.js +++ b/src/components/Filter/index.js @@ -1,59 +1,4 @@ -import Filter from "./Filter"; -import * as fields from "./fields"; -import getFilterValue from "./getFilterValue"; -import {useContext as useFilter} from "./context"; -import withFilterValue from "./withFilterValue"; -import SearchInput from "./SearchInput"; -import FilterProvider from './FilterProvider'; -import pickSelectValues from "./pickSelectValues"; -import createFilterValueMapper from "./createFilterValueMapper"; -import useUrlFilter, {createUrlParamsReader, stripConsumedUrlParams} from "./useUrlFilter"; -import useUrlFilterValue from "./useUrlFilterValue"; -import filterToUrlParams, {parseFilterEntry, takeFilterEntry, createUrlFilterReader} from "./filterToUrlParams"; -import filterInterceptors, {singleSelectInterceptor, multiSelectInterceptor} from "./filterInterceptors"; +import '@kne/react-filter/dist/index.css'; -Filter.fields = fields; -Filter.getFilterValue = getFilterValue; -Filter.useFilter = useFilter; -Filter.SearchInput = SearchInput; -Filter.withFilterValue = withFilterValue; -Filter.FilterProvider = FilterProvider; -Filter.pickSelectValues = pickSelectValues; -Filter.createFilterValueMapper = createFilterValueMapper; -Filter.useUrlFilter = useUrlFilter; -Filter.useUrlFilterValue = useUrlFilterValue; -Filter.createUrlParamsReader = createUrlParamsReader; -Filter.stripConsumedUrlParams = stripConsumedUrlParams; -Filter.filterToUrlParams = filterToUrlParams; -Filter.parseFilterEntry = parseFilterEntry; -Filter.takeFilterEntry = takeFilterEntry; -Filter.createUrlFilterReader = createUrlFilterReader; -Filter.filterInterceptors = filterInterceptors; -Filter.singleSelectInterceptor = singleSelectInterceptor; -Filter.multiSelectInterceptor = multiSelectInterceptor; -export default Filter; -export {fields, getFilterValue, useFilter, withFilterValue, SearchInput, FilterProvider, pickSelectValues, createFilterValueMapper, useUrlFilter, useUrlFilterValue, createUrlParamsReader, stripConsumedUrlParams, filterToUrlParams, parseFilterEntry, takeFilterEntry, createUrlFilterReader, filterInterceptors, singleSelectInterceptor, multiSelectInterceptor}; -export {default as AdvancedFilter, advancedFields} from "./AdvancedFilter"; -export {default as FilterValueDisplay} from "./FilterValueDisplay"; -export {default as FilterItem} from "./FilterItem"; -export {default as FilterLines} from "./FilterLines"; -export {default as PopoverItem} from "./PopoverItem"; -export {default as withFieldItem} from "./withFieldItem"; -export {default as FilterItemContainer} from "./FilterItemContainer"; -export { - NumberRangeFilterItem, - InputFilterItem, - CityFilterItem, - AdvancedSelectFilterItem, - SuperSelectFilterItem, - SuperSelectTableListFilterItem, - SuperSelectUserFilterItem, - UserFilterItem, - FunctionSelectFilterItem, - IndustrySelectFilterItem, - CascaderFilterItem, - TreeFilterItem, - DatePickerFilterItem, - DateRangePickerFilterItem, - TypeDateRangePickerFilterItem, -} from "./fields"; +export * from "@kne/react-filter"; +export {default} from "@kne/react-filter"; \ No newline at end of file diff --git a/src/components/Filter/locale/en-US.js b/src/components/Filter/locale/en-US.js deleted file mode 100644 index fd83acf0..00000000 --- a/src/components/Filter/locale/en-US.js +++ /dev/null @@ -1,21 +0,0 @@ -import formInfoMessage from "@components/FormInfo/locale/en-US.js"; -import commonMessage from "@common/components/locale/en-US.js"; - -const message = { - filterText: "Filter", - moreText: "More", - selectedText: "Selected", - clearAllText: "Clear All", - toggleUpText: "Pack Up", - selectedTextAdvanced: "Selected", - clearText: "Clear Filter", - otherText: "Other", - cancelText: "Cancel", - determineText: "Determine", - year: "year", - over: "over {count} {unit}s", - lessThan: "less than {count} {unit}s", - inputPlaceholder: "Please enter {label}" -}; - -export default Object.assign({}, commonMessage, formInfoMessage, message); diff --git a/src/components/Filter/locale/index.js b/src/components/Filter/locale/index.js deleted file mode 100644 index 1a49770b..00000000 --- a/src/components/Filter/locale/index.js +++ /dev/null @@ -1,8 +0,0 @@ -const importMessages = (locale) => { - return { - "en-US": () => import("./en-US"), - "zh-CN": () => import("./zh-CN"), - }[locale](); -}; - -export default importMessages; diff --git a/src/components/Filter/locale/zh-CN.js b/src/components/Filter/locale/zh-CN.js deleted file mode 100644 index e81bc6c5..00000000 --- a/src/components/Filter/locale/zh-CN.js +++ /dev/null @@ -1,21 +0,0 @@ -import formInfoMessage from "@components/FormInfo/locale/zh-CN.js"; -import commonMessage from "@common/components/locale/zh-CN.js"; - -const message = { - filterText: "筛选", - moreText: "更多", - selectedText: "您已选择", - clearAllText: "清空全部", - toggleUpText: "收起", - selectedTextAdvanced: "已选", - clearText: "清空筛选", - otherText: "其他", - cancelText: "取消", - determineText: "确定", - year: "年", - over: "{count}{unit}以上", - lessThan: "{count}{unit}以下", - inputPlaceholder: "请输入{label}" -}; - -export default Object.assign({}, commonMessage, formInfoMessage, message); diff --git a/src/components/Filter/pickSelectValues.js b/src/components/Filter/pickSelectValues.js deleted file mode 100644 index c6f57428..00000000 --- a/src/components/Filter/pickSelectValues.js +++ /dev/null @@ -1,39 +0,0 @@ -/** - * 从筛选值中提取原始值数组。 - * 支持:原始值、{ value } 对象、{ id } 对象、以及它们的数组。 - * - * @example - * pickSelectValues([{ value: 1 }, { id: 2 }, '3']) - * // => ['1', '2', '3'] - * - * pickSelectValues({ value: 'open' }) - * // => ['open'] - * - * pickSelectValues(null) - * // => [] - */ -const pickSelectValues = value => { - if (value == null || value === '') { - return []; - } - const list = Array.isArray(value) ? value : [value]; - return list - .map(item => { - if (item == null) { - return null; - } - if (typeof item !== 'object') { - return String(item); - } - if (item.value != null && item.value !== '') { - return String(item.value); - } - if (item.id != null && item.id !== '') { - return String(item.id); - } - return null; - }) - .filter(Boolean); -}; - -export default pickSelectValues; diff --git a/src/components/Filter/style.module.scss b/src/components/Filter/style.module.scss deleted file mode 100644 index d1ccbc0a..00000000 --- a/src/components/Filter/style.module.scss +++ /dev/null @@ -1,468 +0,0 @@ -.filter { - padding: 0 !important; - border: none !important; - font-size: var(--font-size-small) !important; -} - -.filter-item-icon { - display: block; - font-size: var(--font-size-small) !important; - transform: scale(0.5); -} - -.filter-item-option-icon { - display: block; - font-size: var(--font-size-small) !important; - transform: scale(0.83); -} - -.filter-title { - width: 100%; - padding: 8px var(--padding-width); - line-height: 32px; - border-bottom: solid 1px rgba(126, 134, 142, 0.16); - - > :global(.ant-space-item:last-child) { - flex: 1; - } -} - -.filter-title-hidden { - height: 0; - width: 0; - left: 0; - top: 0; - overflow: hidden; - position: absolute; - padding: 0; - line-height: 0; -} - -.ad-filter-title { - width: 100%; - line-height: 32px; - - .ad-filter-line { - line-height: 32px; - } - - .filter-line .clean-btn { - border-color: var(--font-color-grey-2); - } - - > :global(.ant-space-item:last-child) { - flex: 1; - } - - .filter-item.un-expand { - border: solid 1px var(--font-color-grey-2); - } -} - -.ad-filter-selected { - border-top: solid 1px var(--bg-color-grey-2); - padding: 6px 0; - font-size: var(--font-size-small) !important; -} - -.filter-label { - display: block; - font-weight: bold; - white-space: nowrap; - font-size: var(--font-size-small) !important; -} - -.filter-line { - display: flex; - align-items: center; - flex-wrap: wrap; - position: relative; - min-height: 32px; - gap: 8px; - - .clean-btn { - color: var(--font-color-grey); - border-color: transparent; - - &:hover { - color: var(--primary-color); - } - } -} - -.ad-filter-line { - line-height: 24px; - margin-bottom: 6px; - - input { - font-size: var(--font-size-small) !important; - } - - .filter-item { - padding: 0 4px; - } -} - -.filter-item-wrap { - height: 32px; -} - -.filter-item { - //width: 100%; - position: relative; - height: 20px; - overflow: hidden; - color: var(--font-color-grey); - border-radius: var(--radius-default, 2px); - padding: 0 var(--font-size-small); - transition: color 300ms, background 300ms; - cursor: pointer; - - :global { - .ant-space-item { - font-size: 12px; - line-height: 20px; - } - } - - &.option { - color: var(--primary-color); - width: auto; - } - - &:hover { - //color: var(--primary-color); - background: var(--bg-color-grey-1); - } - - &.is-active { - color: var(--primary-color); - } - - &.is-visited { - color: #fff; - background: var(--primary-color); - - .filter-item-icon { - transform: rotate(180deg) scale(0.5); - } - } - - :global(.advance-select-input) { - width: 100%; - height: 100%; - overflow: hidden; - } -} - -.un-expand-shadow { - opacity: 0; - pointer-events: none; - height: 20px; - overflow: hidden; - - :global(.ant-btn) { - font-size: var(--font-size-small) !important; - } -} - -.un-expand { - position: absolute; - right: 0; - bottom: 0; - - :global(.ant-btn) { - font-size: var(--font-size-small) !important; - } -} - -.filter-item-label { - white-space: nowrap; - display: flex; - font-size: var(--font-size-small) !important; -} - -.filter-item-field { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - opacity: 0; - overflow: hidden; - display: inline-flex; - - input, - :global(.ant-picker-range-separator), - :global(.ant-picker-separator) { - cursor: pointer; - } - - .filter-drop-tag { - flex: 1; - } -} - -.filter-list-tag { - line-height: 18px; - padding: 0 4px; -} - -.filter-drop-tag { - line-height: 18px; - padding: 0 4px; - margin: 0; -} - -.filter-tag { - vertical-align: middle; - display: flex; - align-items: center; - background: #5cb8b210; - border: none; - margin-top: 2px; - margin-bottom: 2px; - color: var(--font-color-grey); - - :global(.ant-tag-close-icon) { - color: var(--primary-color); - } - - :global(.ant-tag) { - color: var(--font-color-grey); - } -} - -.filter-tag-value { - color: var(--primary-color); - max-width: 360px; - display: block; - text-overflow: ellipsis; - overflow: hidden; - margin-left: 4px; - - :global { - .ant-space { - font-size: 12px; - } - } -} - -.pop-util-content { - padding: 24px; -} - -.date-range-picker-popup { - :global(.ant-picker-range-arrow) { - display: none !important; - } - - :global(.ant-picker-panel-container) { - transform: translateY(-10px); - } -} - -.pop-util-overlay { - padding: 0; - - :global(.ant-popover-arrow) { - display: none; - } - - :global(.ant-popover-container) { - padding: 0; - transform: translateY(-6px); - } - - :global(.ant-popover-inner-content), - :global(.ant-popover-inner) { - padding: 0; - } - - :global(.ant-popover-inner) { - transform: translateY(-10px); - } - - :global { - .ant-input-number-focused { - box-shadow: none; - } - } -} - -.pop-util-footer { - border-top: solid 1px var(--bg-color-grey-3); - padding: 8px 24px; -} - -.pop-util-text { - width: 100%; - height: 100%; -} - -.filter-item-inner { - height: 100%; - min-height: unset !important; -} - -.filter-item-text { - width: 240px; -} - -.filter-item-number-range { - width: 120px; -} - -.filter-item-number-range { - //width: 240px; -} - -.filter-item-custom { - display: inline-flex; - - .is-active { - color: var(--font-color); - } - - .filter-item-label { - font-size: var(--font-size-small) !important; - color: var(--font-color); - } - - .filter-item:hover { - background-color: transparent; - - .filter-item-label { - color: var(--primary-color); - } - } - - :global { - .ant-popover-open { - background-color: var(--primary-color); - - .filter-item-label { - color: #fff; - } - } - - .iconfont { - display: none; - } - } -} - -.filter-item-custom-active { - .filter-item { - background-color: var(--primary-color); - } - - .filter-item-label { - gap: 0 !important; - color: #fff; - } -} - -.filter-item-number-range-split:global(.ant-input) { - width: 32px; - border-left: 0; - border-right: 0; - pointer-events: none; - background: transparent; -} - -.filter-item-number-input { - &:first-child { - width: 105px; - - &:not(:global(.ant-input-number-focused)):not(:hover) { - border-right-color: transparent; - } - } - - :global(.ant-input-number) { - width: 105px; - - &:not(:global(.ant-input-number-focused)):not(:hover) { - border-left-color: transparent; - } - } - - :global(.ant-input-number-group-addon:last-child) { - border-left: 1px solid #d9d9d9; - } -} - -.range-picker { - :global { - .ant-picker-input { - display: none; - } - } -} - -.cascader-select-wrap { - :global { - .ant-select-selector { - padding-left: 11px; - } - - .ant-select-selection-item { - font-size: 12px; - height: 20px; - line-height: 20px; - align-items: center; - display: flex; - vertical-align: middle; - background: rgba(#222222, 0.02); - border: 1px solid #d9d9d9; - border-radius: var(--radius-default, 2px); - } - } -} - -.cascader-dropdown-menu { - font-size: 14px; -} - -.filter-advanced { - padding: 24px 16px 18px; - border-bottom: solid 1px rgba(126, 134, 142, 0.16); - - .filter-label { - min-width: 60px; - line-height: 22px; - } - - .filter-item-wrap { - height: auto; - } - - .filter-title { - line-height: unset; - } - - .filter-line { - min-height: auto; - } -} - -.filter-advanced-item-other { - position: relative; - overflow: hidden; -} - -.filter-advanced-item-other-inner { - position: absolute !important; - top: 0; - left: 0; - right: 0; - bottom: 0; - opacity: 0; - min-width: auto !important; -} - -.filter-advanced-more { - padding: 0 !important; - border-bottom: none !important; -} diff --git a/src/components/Filter/useUrlFilter.js b/src/components/Filter/useUrlFilter.js deleted file mode 100644 index f9421e40..00000000 --- a/src/components/Filter/useUrlFilter.js +++ /dev/null @@ -1,93 +0,0 @@ -import {useState, useRef, useEffect} from 'react'; -import {useSearchParams} from 'react-router-dom'; - -/** - * 创建 URL 参数读取器,自动追踪已消费的参数 key。 - * - * @param {URLSearchParams} searchParams - React Router 的 searchParams 对象 - * @returns {{ take: (key: string) => string|null, getConsumedKeys: () => string[] }} - * - * @example - * const { take, getConsumedKeys } = createUrlParamsReader(searchParams); - * const orgId = take('tenantOrgId'); - * const orgName = take('orgName'); - * // getConsumedKeys() => ['tenantOrgId', 'orgName'] - */ -export const createUrlParamsReader = (searchParams) => { - const consumedKeys = []; - const take = (key) => { - if (!searchParams.has(key)) return null; - consumedKeys.push(key); - return searchParams.get(key); - }; - const getConsumedKeys = () => consumedKeys; - return {take, getConsumedKeys}; -}; - -/** - * 从 URL 参数中移除已消费的 key,返回新的 URLSearchParams 或 null(无变化时)。 - * - * @param {URLSearchParams} searchParams - 当前 URL 参数 - * @param {string[]} consumedKeys - 需要移除的 key 列表 - * @returns {URLSearchParams|null} - */ -export const stripConsumedUrlParams = (searchParams, consumedKeys) => { - if (!consumedKeys?.length) return null; - const next = new URLSearchParams(searchParams); - let changed = false; - consumedKeys.forEach(key => { - if (next.has(key)) { - next.delete(key); - changed = true; - } - }); - return changed ? next : null; -}; - -/** - * 从 URL 参数初始化 Filter 状态的 hook。 - * - * 读取 URL 参数构建初始筛选值,并在挂载后自动清除已消费的 URL 参数。 - * - * @param {Object} options - * @param {Function} options.readUrlParams - 读取 URL 参数并返回 { consumedKeys: string[], ...data } - * @param {Function} options.buildFilter - 接收 readUrlParams 的返回值,构建初始 filter 数组 - * @returns {[Array, Function]} - [filter, setFilter] - * - * @example - * const [filter, setFilter] = useUrlFilter({ - * readUrlParams: (searchParams) => { - * const { take, getConsumedKeys } = createUrlParamsReader(searchParams); - * const orgId = take('tenantOrgId'); - * return { consumedKeys: getConsumedKeys(), orgId }; - * }, - * buildFilter: ({ orgId }) => [ - * { name: 'status', value: { label: '开启', value: 'open' } }, - * ...(orgId ? [{ name: 'tenantOrgId', value: { label: orgId, value: orgId } }] : []) - * ] - * }); - */ -const useUrlFilter = ({readUrlParams, buildFilter}) => { - const [searchParams, setSearchParams] = useSearchParams(); - const urlFilterSnapshotRef = useRef(null); - - if (urlFilterSnapshotRef.current === null) { - urlFilterSnapshotRef.current = readUrlParams(searchParams); - } - - const [filter, setFilter] = useState(() => buildFilter(urlFilterSnapshotRef.current)); - - const urlStrippedRef = useRef(false); - useEffect(() => { - if (urlStrippedRef.current) return; - urlStrippedRef.current = true; - const nextParams = stripConsumedUrlParams(searchParams, urlFilterSnapshotRef.current?.consumedKeys); - if (nextParams) { - setSearchParams(nextParams, {replace: true}); - } - }, [searchParams, setSearchParams]); - - return [filter, setFilter]; -}; - -export default useUrlFilter; diff --git a/src/components/Filter/useUrlFilterValue.js b/src/components/Filter/useUrlFilterValue.js deleted file mode 100644 index 52dc8522..00000000 --- a/src/components/Filter/useUrlFilterValue.js +++ /dev/null @@ -1,77 +0,0 @@ -import useUrlFilter from './useUrlFilter'; -import {createUrlFilterReader} from './filterToUrlParams'; - -/** - * 从 URL 参数初始化 Filter 状态的 hook(简化版)。 - * - * 基于 useUrlFilter 封装,使用 createUrlFilterReader 解析 filterParams[key] 格式的 URL 参数, - * 自动解析 label:value 格式,支持单选和多选。 - * - * @param {string[]|Object} mapping - URL 参数映射 - * - 数组: ['tenantOrgId', 'orgName'],默认单选,自动创建 { name: key, value: { label, value } } - * - 对象: - * - 值为 true: 单选,使用默认转换 { name: key, value: { label, value } } - * - 值为 { multi: true }: 多选,value 为 [{ label, value }, ...] 数组 - * - 值为函数: 自定义转换,接收解析后的值(单选为 { label, value },多选为数组),返回 filter 项或 null/falsy 跳过 - * @returns {[Array, Function]} - [filter, setFilter] - * - * @example - * // 数组形式(默认单选) - * const [filter, setFilter] = useUrlFilterValue(['keyword', 'status']); - * // URL: ?filterParams[keyword]=前端开发&filterParams[status]=招聘中:active - * // → filter: [ - * // { name: 'keyword', value: { label: '前端开发', value: '前端开发' } }, - * // { name: 'status', value: { label: '招聘中', value: 'active' } } - * // ] - * - * @example - * // 对象形式(多选 + 自定义) - * const [filter, setFilter] = useUrlFilterValue({ - * keyword: true, - * city: { multi: true }, - * status: (parsed) => parsed ? { name: 'status', value: parsed } : null - * }); - * // URL: ?filterParams[keyword]=测试&filterParams[city]=上海:010,北京:020 - * // → keyword: { label: '测试', value: '测试' } - * // → city: [{ label: '上海', value: '010' }, { label: '北京', value: '020' }] - */ -const useUrlFilterValue = (mapping) => { - const normalizedMapping = Array.isArray(mapping) - ? mapping.reduce((acc, item) => { - const key = typeof item === 'string' ? item : item.name; - acc[key] = typeof item === 'string' ? true : item; - return acc; - }, {}) - : mapping; - - return useUrlFilter({ - readUrlParams: (searchParams) => { - const {takeFilterEntry, getConsumedKeys} = createUrlFilterReader(searchParams); - const data = {}; - Object.entries(normalizedMapping).forEach(([key, config]) => { - const multi = typeof config === 'object' && config !== null && config.multi; - const val = takeFilterEntry(key, {multi}); - if (val !== null) data[key] = val; - }); - return {consumedKeys: getConsumedKeys(), ...data}; - }, - buildFilter: (data) => { - const {consumedKeys, ...values} = data; - const filters = []; - Object.entries(normalizedMapping).forEach(([key, config]) => { - if (values[key] === undefined) return; - const parsedValue = values[key]; - if (typeof config === 'function') { - const result = config(parsedValue); - if (result) filters.push(result); - } else { - const label = typeof config === 'object' && config !== null ? config.label : key; - filters.push({name: key, label, value: parsedValue}); - } - }); - return filters; - } - }); -}; - -export default useUrlFilterValue; diff --git a/src/components/Filter/withFieldItem.js b/src/components/Filter/withFieldItem.js deleted file mode 100644 index 7e6744d0..00000000 --- a/src/components/Filter/withFieldItem.js +++ /dev/null @@ -1,24 +0,0 @@ -import {useState} from "react"; -import isNotEmpty from "@common/utils/isNotEmpty"; -import FilterItem from "./FilterItem"; -import style from "./style.module.scss"; - -const withFieldItem = (WrappedComponent) => ({value, onChange, interceptor, label, render, ...props}) => { - const [open, setOpen] = useState(false); - const renderChildren = (otherProps) => onChange(interceptor.output(...args)) : onChange} - valueType="all" - onOpenChange={setOpen} - />; - return ( - {typeof render === "function" ? render({ - children: renderChildren - }) : renderChildren()} - ); -}; - -export default withFieldItem; diff --git a/src/components/Filter/withFilterValue.js b/src/components/Filter/withFilterValue.js deleted file mode 100644 index 7c16350b..00000000 --- a/src/components/Filter/withFilterValue.js +++ /dev/null @@ -1,27 +0,0 @@ -import { useContext } from "./context"; -import get from "lodash/get"; - -const withFilterValue = (WrappedComponent) => { - return ({ name, label, ...props }) => { - const { value, onChange } = useContext(); - return ( - - onChange({ - name, - label, - value, - }) - : props.onChange - } - value={value ? get(value.get(name), "value") : props.value} - /> - ); - }; -}; - -export default withFilterValue; diff --git a/src/components/HistoryStore/index.js b/src/components/HistoryStore/index.js index a796d85b..56968be8 100644 --- a/src/components/HistoryStore/index.js +++ b/src/components/HistoryStore/index.js @@ -9,18 +9,21 @@ import dropWhile from "lodash/dropWhile"; import uniqBy from "lodash/uniqBy"; import useClickOutside from "@kne/use-click-outside"; import classnames from "classnames"; +import withLocale from './withLocale'; +import { useIntl } from '@kne/react-intl'; -const HistoryStore = ({ +const HistoryStore = withLocale(({ className, overlayClassName, storeName = 'HISTORY_STORE_KEY', maxLength = 5, - label = '最近搜索', + label, children, onSelect, zIndex, getPopupContainer, }) => { + const { formatMessage } = useIntl(); const [list, setList] = useState(() => { return localStorage.getItem(storeName) || []; }); @@ -99,7 +102,7 @@ const HistoryStore = ({ ref={popoverContentRef} > -
{label}
+
{label || formatMessage({ id: 'RecentSearch' })}
{list.map((item) => ( ); -}; +}); export default HistoryStore; diff --git a/src/components/HistoryStore/locale/en-US.js b/src/components/HistoryStore/locale/en-US.js new file mode 100644 index 00000000..d5b1b948 --- /dev/null +++ b/src/components/HistoryStore/locale/en-US.js @@ -0,0 +1,5 @@ +const message = { + RecentSearch: "Recent Search" +}; + +export default message; diff --git a/src/components/HistoryStore/locale/zh-CN.js b/src/components/HistoryStore/locale/zh-CN.js new file mode 100644 index 00000000..75ba09f5 --- /dev/null +++ b/src/components/HistoryStore/locale/zh-CN.js @@ -0,0 +1,5 @@ +const message = { + RecentSearch: "最近搜索" +}; + +export default message; diff --git a/src/components/HistoryStore/withLocale.js b/src/components/HistoryStore/withLocale.js new file mode 100644 index 00000000..ecd3496e --- /dev/null +++ b/src/components/HistoryStore/withLocale.js @@ -0,0 +1,11 @@ +import {createWithIntlProvider} from '@kne/react-intl'; +import zhCN from './locale/zh-CN'; +import enUS from './locale/en-US'; + +const withLocale = createWithIntlProvider({ + defaultLocale: 'zh-CN', messages: { + 'zh-CN': zhCN, 'en-US': enUS + }, namespace: 'HistoryStore' +}); + +export default withLocale;