diff --git a/package.json b/package.json index 1b8f911..3d2a977 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne-components/components-core", - "version": "0.4.74", + "version": "0.4.75", "files": [ "build" ], diff --git a/src/components/Filter/README.md b/src/components/Filter/README.md index 96dc004..4b350a4 100644 --- a/src/components/Filter/README.md +++ b/src/components/Filter/README.md @@ -947,6 +947,7 @@ const { getFilterValue, createFilterValueMapper, pickSelectValues, + useUrlFilterValue, } = _Filter; const { useState, useMemo } = React; const { Space, Card, Divider, Typography, Button, Alert, Tag } = antd; @@ -1150,6 +1151,67 @@ 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 }
+});`}
+        
+
); }; @@ -1901,6 +1963,56 @@ const [filter, setFilter] = useUrlFilter({ }); ``` +#### 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 跳过 + +**返回值:** + +| 返回值 | 说明 | 类型 | +|--------|------|------| +| [0] | 初始筛选值数组 | array | +| [1] | 设置筛选值的函数 | function | + +**示例:** +```javascript +// 数组形式(默认单选) +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。 diff --git a/src/components/Filter/doc/api.md b/src/components/Filter/doc/api.md index 1205f4f..55a54bd 100644 --- a/src/components/Filter/doc/api.md +++ b/src/components/Filter/doc/api.md @@ -573,6 +573,56 @@ const [filter, setFilter] = useUrlFilter({ }); ``` +#### 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 跳过 + +**返回值:** + +| 返回值 | 说明 | 类型 | +|--------|------|------| +| [0] | 初始筛选值数组 | array | +| [1] | 设置筛选值的函数 | function | + +**示例:** +```javascript +// 数组形式(默认单选) +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。 diff --git a/src/components/Filter/doc/use-url-filter.js b/src/components/Filter/doc/use-url-filter.js index 5ca474b..63d964c 100644 --- a/src/components/Filter/doc/use-url-filter.js +++ b/src/components/Filter/doc/use-url-filter.js @@ -10,6 +10,7 @@ const { getFilterValue, createFilterValueMapper, pickSelectValues, + useUrlFilterValue, } = _Filter; const { useState, useMemo } = React; const { Space, Card, Divider, Typography, Button, Alert, Tag } = antd; @@ -213,6 +214,67 @@ 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 }
+});`}
+        
+
); }; diff --git a/src/components/Filter/index.js b/src/components/Filter/index.js index e2fe066..6ee292b 100644 --- a/src/components/Filter/index.js +++ b/src/components/Filter/index.js @@ -8,6 +8,7 @@ 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"; @@ -20,6 +21,7 @@ Filter.FilterProvider = FilterProvider; Filter.pickSelectValues = pickSelectValues; Filter.createFilterValueMapper = createFilterValueMapper; Filter.useUrlFilter = useUrlFilter; +Filter.useUrlFilterValue = useUrlFilterValue; Filter.createUrlParamsReader = createUrlParamsReader; Filter.stripConsumedUrlParams = stripConsumedUrlParams; Filter.filterToUrlParams = filterToUrlParams; @@ -30,7 +32,7 @@ Filter.filterInterceptors = filterInterceptors; Filter.singleSelectInterceptor = singleSelectInterceptor; Filter.multiSelectInterceptor = multiSelectInterceptor; export default Filter; -export {fields, getFilterValue, useFilter, withFilterValue, SearchInput, FilterProvider, pickSelectValues, createFilterValueMapper, useUrlFilter, createUrlParamsReader, stripConsumedUrlParams, filterToUrlParams, parseFilterEntry, takeFilterEntry, createUrlFilterReader, filterInterceptors, singleSelectInterceptor, multiSelectInterceptor}; +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"; diff --git a/src/components/Filter/useUrlFilter.js b/src/components/Filter/useUrlFilter.js index e6ba26e..f9421e4 100644 --- a/src/components/Filter/useUrlFilter.js +++ b/src/components/Filter/useUrlFilter.js @@ -1,5 +1,5 @@ -import { useState, useRef, useEffect } from 'react'; -import { useSearchParams } from 'react-router-dom'; +import {useState, useRef, useEffect} from 'react'; +import {useSearchParams} from 'react-router-dom'; /** * 创建 URL 参数读取器,自动追踪已消费的参数 key。 @@ -14,14 +14,14 @@ import { useSearchParams } from 'react-router-dom'; * // 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 }; + const consumedKeys = []; + const take = (key) => { + if (!searchParams.has(key)) return null; + consumedKeys.push(key); + return searchParams.get(key); + }; + const getConsumedKeys = () => consumedKeys; + return {take, getConsumedKeys}; }; /** @@ -32,16 +32,16 @@ export const createUrlParamsReader = (searchParams) => { * @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; + 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; }; /** @@ -67,27 +67,27 @@ export const stripConsumedUrlParams = (searchParams, consumedKeys) => { * ] * }); */ -const useUrlFilter = ({ readUrlParams, buildFilter }) => { - const [searchParams, setSearchParams] = useSearchParams(); - const urlFilterSnapshotRef = useRef(null); +const useUrlFilter = ({readUrlParams, buildFilter}) => { + const [searchParams, setSearchParams] = useSearchParams(); + const urlFilterSnapshotRef = useRef(null); - if (urlFilterSnapshotRef.current === null) { - urlFilterSnapshotRef.current = readUrlParams(searchParams); - } + if (urlFilterSnapshotRef.current === null) { + urlFilterSnapshotRef.current = readUrlParams(searchParams); + } - const [filter, setFilter] = useState(() => buildFilter(urlFilterSnapshotRef.current)); + 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]); + 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]; + return [filter, setFilter]; }; export default useUrlFilter; diff --git a/src/components/Filter/useUrlFilterValue.js b/src/components/Filter/useUrlFilterValue.js new file mode 100644 index 0000000..52dc852 --- /dev/null +++ b/src/components/Filter/useUrlFilterValue.js @@ -0,0 +1,77 @@ +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/Table/useSelectedRow.js b/src/components/Table/useSelectedRow.js index a89b72e..bf97dc5 100644 --- a/src/components/Table/useSelectedRow.js +++ b/src/components/Table/useSelectedRow.js @@ -39,7 +39,9 @@ const useSelectedRow = (options) => { return newValue; }); } - }, setSelectedRows + }, setSelectedRows, clearSelectedRows: () => { + setSelectedRows([]); + } }; };