From cc27485fc029827459068e0d85ff6a8c567bd80f Mon Sep 17 00:00:00 2001 From: mantou132 <709922234@qq.com> Date: Tue, 23 Jan 2024 01:39:00 +0800 Subject: [PATCH] [duoyun-ui] Fixed `` search --- ...-analyzer.yml => gem-analyzer-publish.yml} | 0 packages/duoyun-ui/docs/en/01-guide/README.md | 14 +- .../docs/en/02-elements/cascader-picker.md | 15 +- .../duoyun-ui/docs/en/02-elements/cascader.md | 15 +- .../docs/en/02-elements/color-panel.md | 15 +- .../docs/en/02-elements/color-picker.md | 26 ++- .../docs/en/02-elements/date-panel.md | 15 +- .../docs/en/02-elements/date-picker.md | 15 +- .../docs/en/02-elements/date-range-panel.md | 15 +- .../docs/en/02-elements/date-range-picker.md | 15 +- .../docs/en/02-elements/time-panel.md | 15 +- .../docs/en/02-elements/time-picker.md | 15 +- .../docs/zh/02-elements/time-panel.md | 2 +- .../duoyun-ui/docs/zh/30-blog/200-crud.md | 75 +++++-- packages/duoyun-ui/package.json | 2 +- packages/duoyun-ui/src/elements/checkbox.ts | 1 + packages/duoyun-ui/src/elements/list.ts | 12 +- packages/duoyun-ui/src/elements/radio.ts | 3 +- packages/duoyun-ui/src/elements/table.ts | 2 + packages/duoyun-ui/src/helper/store.ts | 19 +- packages/duoyun-ui/src/lib/element.ts | 30 +++ packages/duoyun-ui/src/patterns/console.ts | 2 +- packages/duoyun-ui/src/patterns/form.ts | 18 +- packages/duoyun-ui/src/patterns/table.ts | 191 ++++++++++-------- packages/gem-examples/src/console/index.ts | 5 +- packages/gem-examples/src/console/item.ts | 88 ++++---- .../001-guide/001-basic/006-styled-element.md | 13 +- 27 files changed, 406 insertions(+), 232 deletions(-) rename .github/workflows/{gem-analyzer.yml => gem-analyzer-publish.yml} (100%) diff --git a/.github/workflows/gem-analyzer.yml b/.github/workflows/gem-analyzer-publish.yml similarity index 100% rename from .github/workflows/gem-analyzer.yml rename to .github/workflows/gem-analyzer-publish.yml diff --git a/packages/duoyun-ui/docs/en/01-guide/README.md b/packages/duoyun-ui/docs/en/01-guide/README.md index d4b57221..f246a4eb 100644 --- a/packages/duoyun-ui/docs/en/01-guide/README.md +++ b/packages/duoyun-ui/docs/en/01-guide/README.md @@ -39,15 +39,21 @@ Toast.open('error', 'An error occurred'); DuoyunUI supports an ESM method independently uses an element, such as adding a keyboard access to your website(press f list all focusable elements): ```ts -import('https://esm.sh/duoyun-ui/elements/keyboard-access').then(({ DuoyunKeyboardAccessElement }) => - document.body.append(new DuoyunKeyboardAccessElement()), +import('https://esm.sh/duoyun-ui/elements/keyboard-access').then( + ({ DuoyunKeyboardAccessElement }) => + document.body.append(new DuoyunKeyboardAccessElement()), ); ``` For example, enable input recording mode: ```ts -import('https://esm.sh/duoyun-ui/elements/input-capture').then(({ DuoyunInputCaptureElement }) => - document.body.append(new DuoyunInputCaptureElement()), +import('https://esm.sh/duoyun-ui/elements/input-capture').then( + ({ DuoyunInputCaptureElement }) => + document.body.append(new DuoyunInputCaptureElement()), ); ``` + +## Feedback DuoyunUI + +Please visit [GitHub](https://github.com/mantou132/gem) diff --git a/packages/duoyun-ui/docs/en/02-elements/cascader-picker.md b/packages/duoyun-ui/docs/en/02-elements/cascader-picker.md index 9a38c48e..28bbbf1e 100644 --- a/packages/duoyun-ui/docs/en/02-elements/cascader-picker.md +++ b/packages/duoyun-ui/docs/en/02-elements/cascader-picker.md @@ -1,12 +1,23 @@ # `` -## Example +## `` Example -## API +## `` API + +## `` Example + + + +## `` API + + diff --git a/packages/duoyun-ui/docs/en/02-elements/cascader.md b/packages/duoyun-ui/docs/en/02-elements/cascader.md index 521fee06..1d723071 100644 --- a/packages/duoyun-ui/docs/en/02-elements/cascader.md +++ b/packages/duoyun-ui/docs/en/02-elements/cascader.md @@ -1,12 +1,3 @@ -# `` - -## Example - - - -## API - - +--- +redirect: ./cascader-picker.md +--- diff --git a/packages/duoyun-ui/docs/en/02-elements/color-panel.md b/packages/duoyun-ui/docs/en/02-elements/color-panel.md index faa60512..26cf73d5 100644 --- a/packages/duoyun-ui/docs/en/02-elements/color-panel.md +++ b/packages/duoyun-ui/docs/en/02-elements/color-panel.md @@ -1,12 +1,3 @@ -# `` - -## Example - - - -## API - - +--- +redirect: ./color-picker.md +--- diff --git a/packages/duoyun-ui/docs/en/02-elements/color-picker.md b/packages/duoyun-ui/docs/en/02-elements/color-picker.md index 51f92d59..7a3ec9b1 100644 --- a/packages/duoyun-ui/docs/en/02-elements/color-picker.md +++ b/packages/duoyun-ui/docs/en/02-elements/color-picker.md @@ -1,11 +1,15 @@ # `` -## Example +## `` Example ```json [ + { + "value": "#418eec", + "disabled": true + }, { "alpha": true, "value": "#e5e", @@ -16,6 +20,24 @@ -## API +## `` API + +## `` Example + + + +```json +{ + "alpha": true, + "value": "#e5e", + "@change": "(evt) => evt.target.value = evt.detail" +} +``` + + + +## `` API + + diff --git a/packages/duoyun-ui/docs/en/02-elements/date-panel.md b/packages/duoyun-ui/docs/en/02-elements/date-panel.md index 6db9134f..7f269702 100644 --- a/packages/duoyun-ui/docs/en/02-elements/date-panel.md +++ b/packages/duoyun-ui/docs/en/02-elements/date-panel.md @@ -1,12 +1,3 @@ -# `` - -## Example - - - -## API - - +--- +redirect: ./date-picker.md +--- diff --git a/packages/duoyun-ui/docs/en/02-elements/date-picker.md b/packages/duoyun-ui/docs/en/02-elements/date-picker.md index 948e2c38..314ac87b 100644 --- a/packages/duoyun-ui/docs/en/02-elements/date-picker.md +++ b/packages/duoyun-ui/docs/en/02-elements/date-picker.md @@ -1,6 +1,6 @@ # `` -## Example +## `` Example @@ -25,6 +25,17 @@ -## API +## `` API + +## `` Example + + + +## `` API + + diff --git a/packages/duoyun-ui/docs/en/02-elements/date-range-panel.md b/packages/duoyun-ui/docs/en/02-elements/date-range-panel.md index 19aee9a8..ad1ab169 100644 --- a/packages/duoyun-ui/docs/en/02-elements/date-range-panel.md +++ b/packages/duoyun-ui/docs/en/02-elements/date-range-panel.md @@ -1,12 +1,3 @@ -# `` - -## Example - - - -## API - - +--- +redirect: ./date-range-picker.md +--- diff --git a/packages/duoyun-ui/docs/en/02-elements/date-range-picker.md b/packages/duoyun-ui/docs/en/02-elements/date-range-picker.md index e7a23e29..f0951faa 100644 --- a/packages/duoyun-ui/docs/en/02-elements/date-range-picker.md +++ b/packages/duoyun-ui/docs/en/02-elements/date-range-picker.md @@ -1,6 +1,6 @@ # `` -## Example +## `` Example @@ -21,6 +21,17 @@ -## API +## `` API + +## `` Example + + + +## `` API + + diff --git a/packages/duoyun-ui/docs/en/02-elements/time-panel.md b/packages/duoyun-ui/docs/en/02-elements/time-panel.md index bac4be86..9c5d3737 100644 --- a/packages/duoyun-ui/docs/en/02-elements/time-panel.md +++ b/packages/duoyun-ui/docs/en/02-elements/time-panel.md @@ -1,12 +1,3 @@ -# `` - -## Example - - - -## API - - +--- +redirect: ./time-picker.md +--- diff --git a/packages/duoyun-ui/docs/en/02-elements/time-picker.md b/packages/duoyun-ui/docs/en/02-elements/time-picker.md index 603f86ff..946b4cee 100644 --- a/packages/duoyun-ui/docs/en/02-elements/time-picker.md +++ b/packages/duoyun-ui/docs/en/02-elements/time-picker.md @@ -1,12 +1,23 @@ # `` -## Example +## `` Example -## API +## `` API + +## ``Example + + + +## `` API + + diff --git a/packages/duoyun-ui/docs/zh/02-elements/time-panel.md b/packages/duoyun-ui/docs/zh/02-elements/time-panel.md index a71d8a1a..9c5d3737 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/time-panel.md +++ b/packages/duoyun-ui/docs/zh/02-elements/time-panel.md @@ -1,3 +1,3 @@ --- -redirect: time-picker.md +redirect: ./time-picker.md --- diff --git a/packages/duoyun-ui/docs/zh/30-blog/200-crud.md b/packages/duoyun-ui/docs/zh/30-blog/200-crud.md index be5821b6..dfb27496 100644 --- a/packages/duoyun-ui/docs/zh/30-blog/200-crud.md +++ b/packages/duoyun-ui/docs/zh/30-blog/200-crud.md @@ -54,7 +54,6 @@ const routes = { pattern: '/items/:id', title: 'Item Page', async getContent(params) { - // import('./item'); return html` ${JSON.stringify(params)} `; @@ -80,15 +79,18 @@ const routes = { pattern: '/', title: 'Home', getContent(_, ele) { - createRoot(ele).render(<>Home); + ele.react?.unmount(); + ele.react = createRoot(ele); + ele.react.render(<>Home); }, }, item: { pattern: '/items/:id', title: 'Item Page', async getContent(params, ele) { - // await import('./item'); - createRoot(ele).render(<>{JSON.stringify(params)}); + ele.react?.unmount(); + ele.react = createRoot(ele); + ele.react.render(<>{JSON.stringify(params)}); }, }, } satisfies Routes; @@ -104,6 +106,9 @@ const navItems: NavItems = [ +> [!WARNING] +> 使用 React 渲染页面时,为了更好的和 Gem 兼容,需要先卸载以挂载的 Root 节点,再重新创建 React Root 并渲染。 + ### 定义用户信息和全局菜单 之后,可以指定用户信息用来标识用户,还可以定义一些全局命令,比如切换语言、退出登录: @@ -181,11 +186,9 @@ createRoot(document.body).render( ```ts Gem import { html, GemElement, connectStore, customElement } from '@mantou/gem'; -import { locationStore } from 'duoyun-ui/patterns/console'; import 'duoyun-ui/patterns/table'; @customElement('console-page-item') -@connectStore(locationStore) export class ConsolePageItemElement extends GemElement { render = () => { return html``; @@ -196,12 +199,9 @@ export class ConsolePageItemElement extends GemElement { ```tsx React import { useState, useEffect } from 'react'; import { connect } from '@mantou/gem'; -import { locationStore } from 'duoyun-ui/react/DyPatConsole'; import DyPatTable from 'duoyun-ui/react/DyPatTable'; export function Item() { - const [_, update] = useState({}); - useEffect(() => connect(locationStore, () => update({})), []); return ; } ``` @@ -216,7 +216,6 @@ export function Item() { pattern: '/items/:id', title: 'Item Page', async getContent() { -- // await import('./item'); + await import('./item'); return html``; }, @@ -230,7 +229,6 @@ export function Item() { pattern: '/items/:id', title: 'Item Page', async getContent(params, ele) { -- // import('./item'); - createRoot(ele).render(<>{JSON.stringify(params)}); + const { Item } = await import('./item'); + createRoot(ele).render(); @@ -243,6 +241,9 @@ export function Item() { ### 读取列表并渲染表格 +接下来从后端获取数据并填充表格,URL 参数比如 `id` 可以从 `locationStore` 读取,它由 `` 创建,以响应 App 路由更新,它不会从[正在加载但还未显示的页面](https://gemjs.org/zh/blog/improve-route)中获取更新。 +需要将它和页面绑定,以保证 `id` 改变时页面响应变化。 + ```ts Gem @@ -264,9 +265,14 @@ export class ConsolePageItemElement extends GemElement { }, ]; - mounted = async () => { - const data = await get(`https://jsonplaceholder.typicode.com/users`); - this.setState({ data }); + mounted = () => { + this.effect( + async ([id]) => { + const data = await get(`https://jsonplaceholder.typicode.com/users`); + this.setState({ data }); + }, + () => [locationStore.params.id], + ); }; render = () => { @@ -294,8 +300,9 @@ export function Item() { const [data, updateData] = useState(); useEffect(() => { + // const id = locationStore.params.id; get(`https://jsonplaceholder.typicode.com/users`).then(updateData); - }, []); + }, [locationStore.params.id]); const columns: FilterableColumn[] = [ { @@ -432,7 +439,7 @@ html` Create @@ -446,7 +453,7 @@ html` Create @@ -462,7 +469,7 @@ import { Time } from 'duoyun-ui/lib/time'; import { createPaginationStore } from 'duoyun-ui/helper/store'; import type { FetchEventDetail } from 'duoyun-ui/patterns/table'; -const { store, updatePage } = createPaginationStore({ +const pagination = createPaginationStore({ storageKey: 'users', cacheItems: true, pageContainItem: true, @@ -480,10 +487,40 @@ const fetchList = (args: FetchEventDetail) => { }; const onFetch = ({ detail }: CustomEvent) => { - updatePage(fetchList, detail); + pagination.updatePage(fetchList, detail); }; ``` +
+ 优化搜索结果展示(可选) + + 当在有搜索词和无搜索词之间切换时,页面不能立刻切换到新列表,可以为搜索词分配独立的 `pagination`: + +```ts +@customElement('console-page-item') +export class ConsolePageItemElement extends GemElement { + state = { + pagination: pagination, + paginationMap: new Map([['', pagination]]), + }; + + #onFetch = ({ detail }: CustomEvent) => { + let pagination = this.state.paginationMap.get(detail.searchAndFilterKey); + if (!pagination) { + pagination = createPaginationStore({ + cacheItems: true, + pageContainItem: true, + }); + this.state.paginationMap.set(detail.searchAndFilterKey, pagination); + } + this.setState({ pagination }); + pagination.updatePage(fetchList, detail); + }; +} +``` + +
+ > [!TIP] `` 还支持: > > - 使用 `expandedRowRender` 展开行,`@expand` 获取展开事件 diff --git a/packages/duoyun-ui/package.json b/packages/duoyun-ui/package.json index 16de1af6..445cdebf 100644 --- a/packages/duoyun-ui/package.json +++ b/packages/duoyun-ui/package.json @@ -1,6 +1,6 @@ { "name": "duoyun-ui", - "version": "1.1.12", + "version": "1.1.15", "description": "A lightweight desktop UI component library, implemented using Gem", "keywords": [ "frontend", diff --git a/packages/duoyun-ui/src/elements/checkbox.ts b/packages/duoyun-ui/src/elements/checkbox.ts index f770ece1..fe58e7fc 100644 --- a/packages/duoyun-ui/src/elements/checkbox.ts +++ b/packages/duoyun-ui/src/elements/checkbox.ts @@ -26,6 +26,7 @@ const style = createCSSSheet(css` display: inline-flex; align-items: center; gap: 0.5em; + line-height: 2; } :host([disabled]) { cursor: not-allowed; diff --git a/packages/duoyun-ui/src/elements/list.ts b/packages/duoyun-ui/src/elements/list.ts index ec3957c5..e8dbb562 100644 --- a/packages/duoyun-ui/src/elements/list.ts +++ b/packages/duoyun-ui/src/elements/list.ts @@ -10,6 +10,7 @@ import { emitter, Emitter, boolattribute, + attribute, } from '@mantou/gem/lib/decorators'; import { GemElement, html, TemplateResult } from '@mantou/gem/lib/element'; import { addListener, createCSSSheet, css, LinkedList, LinkedListItem, styled, styleMap } from '@mantou/gem/lib/utils'; @@ -87,6 +88,7 @@ export class DuoyunListElement extends GemElement { @property key?: any; // 除了 items 提供另外一种方式来更新 @property renderItem?: (item: any) => TemplateResult; @boolattribute debug: boolean; + @attribute itemexportparts: string; /**enable infinite scroll, virtualization render */ @boolattribute infinite: boolean; @@ -274,10 +276,13 @@ export class DuoyunListElement extends GemElement { #appendItems = (items: any[], oldItems?: any[]) => { if (!oldItems) return; + const oldFirst = oldItems.at(0); let beforeHeight = 0; - for (let i = 0; i < items.length; i++) { - if (this.getKey!(items[i]) === this.getKey!(oldItems[0])) break; - if (this.#isLeftItem(i)) beforeHeight += this.#getRowHeight(); + if (oldFirst) { + for (let i = 0; i < items.length; i++) { + if (this.getKey!(items[i]) === this.getKey!(oldFirst)) break; + if (this.#isLeftItem(i)) beforeHeight += this.#getRowHeight(); + } } if (beforeHeight) { // 有向前(上)加载数据,必须是列数的倍数 @@ -398,6 +403,7 @@ export class DuoyunListElement extends GemElement { if (!this.#keyElementMap.has(key)) { const ele = new DuoyunListItemElement(); ele.setAttribute('part', DuoyunListElement.item); + ele.setAttribute('exportparts', this.itemexportparts); ele.addEventListener('resize', this.#onItemResize); ele.addEventListener('show', () => this.itemshow(this.#keyItemMap.get(key))); ele.intersectionRoot = this.scrollContainer; diff --git a/packages/duoyun-ui/src/elements/radio.ts b/packages/duoyun-ui/src/elements/radio.ts index 96fe6ea4..c44893a9 100644 --- a/packages/duoyun-ui/src/elements/radio.ts +++ b/packages/duoyun-ui/src/elements/radio.ts @@ -21,6 +21,7 @@ const style = createCSSSheet(css` display: inline-flex; align-items: center; gap: 0.5em; + line-height: 2; } :host([disabled]) { cursor: not-allowed; @@ -98,7 +99,7 @@ export const groupStyle = createCSSSheet(css` align-items: center; flex-wrap: wrap; } - :host([orientation='horizontal']) { + :host(:not([orientation='vertical'])) { gap: 1em; } :host([orientation='vertical']) { diff --git a/packages/duoyun-ui/src/elements/table.ts b/packages/duoyun-ui/src/elements/table.ts index a6a1bb11..f3125708 100644 --- a/packages/duoyun-ui/src/elements/table.ts +++ b/packages/duoyun-ui/src/elements/table.ts @@ -44,6 +44,8 @@ const style = createCSSSheet(css` width: 100%; table-layout: fixed; border-collapse: collapse; + /* 为啥用户代理在 localhost 下是 normal,但在 StackBlitz 下没有设置? */ + font-size: inherit; } .selection:where([data-selecting], :state(selecting)) ~ table { user-select: none; diff --git a/packages/duoyun-ui/src/helper/store.ts b/packages/duoyun-ui/src/helper/store.ts index 18e5a683..f7ea61ed 100644 --- a/packages/duoyun-ui/src/helper/store.ts +++ b/packages/duoyun-ui/src/helper/store.ts @@ -1,3 +1,5 @@ +import { useStore } from '@mantou/gem/lib/store'; + import { UseCacheStoreOptions, useCacheStore } from '../lib/utils'; type PaginationReq = { @@ -31,8 +33,8 @@ export type PaginationStore = { }; type PaginationStoreOptions = { - // 缓存 key - storageKey: string; + // 持久化缓存 key, 不提供则不进行持久化缓存 + storageKey?: string; // 指定 item 的唯一 key idKey?: keyof T; // 页面数据是否包含 item,如果包含,则不需要调用额外的 updateItem @@ -45,7 +47,7 @@ export function createPaginationStore>(options: Pa const { idKey = 'id', storageKey, cacheItems, pageContainItem, ...rest } = options; const cacheExcludeKeys: (keyof PaginationStore)[] = ['loader']; - if (cacheItems) cacheExcludeKeys.push('items'); + if (!cacheItems) cacheExcludeKeys.push('items'); const initStore: PaginationStore = { total: 0, @@ -53,16 +55,19 @@ export function createPaginationStore>(options: Pa items: {}, loader: {}, }; - const [store, update, saveStore] = useCacheStore>(storageKey, initStore, { - ...rest, - cacheExcludeKeys, - }); + const [store, update, saveStore = () => {}] = storageKey + ? useCacheStore>(storageKey, initStore, { + ...rest, + cacheExcludeKeys, + }) + : useStore(initStore); const changePage = (page: number, content: Partial) => { store.pagination[page] = { ...store.pagination[page], ...content }; update(); }; + // 指定 API 函数和参数来更新 Store const updatePage = async ( request: (req: Req) => Promise>, req: Req, diff --git a/packages/duoyun-ui/src/lib/element.ts b/packages/duoyun-ui/src/lib/element.ts index d17100d9..d66a4c5b 100644 --- a/packages/duoyun-ui/src/lib/element.ts +++ b/packages/duoyun-ui/src/lib/element.ts @@ -31,3 +31,33 @@ export function findScrollContainer(startElement?: HTMLElement | null) { element = element.parentElement || ((element.getRootNode() as ShadowRoot)?.host as HTMLElement); } } + +export function findRanges(root: Node, text: string) { + const reg = new RegExp([...text].map((c) => `\\u{${c.codePointAt(0)!.toString(16)}}`).join(''), 'gui'); + const ranges: Range[] = []; + const nodes: Node[] = [root]; + while (!!nodes.length) { + const node = nodes.pop()!; + switch (node.nodeType) { + case Node.TEXT_NODE: + const matched = node.nodeValue?.matchAll(reg); + if (matched) { + for (const arr of matched) { + if (arr.index !== undefined) { + const range = new Range(); + range.setStart(node, arr.index); + range.setEnd(node, arr.index + text.length); + ranges.push(range); + } + } + } + break; + case Node.ELEMENT_NODE: + if ((node as Element).shadowRoot) nodes.push((node as Element).shadowRoot as Node); + break; + } + if (node.childNodes[0]) nodes.push(node.childNodes[0]); + if (node.nextSibling) nodes.push(node.nextSibling); + } + return ranges; +} diff --git a/packages/duoyun-ui/src/patterns/console.ts b/packages/duoyun-ui/src/patterns/console.ts index e0d4bd63..92223adb 100644 --- a/packages/duoyun-ui/src/patterns/console.ts +++ b/packages/duoyun-ui/src/patterns/console.ts @@ -143,7 +143,7 @@ const style = createCSSSheet( : rules, ); -// 禁止向皮条 +// 禁止橡皮条 const consoleStyle = createCSSSheet(css` ::selection, ::target-text { diff --git a/packages/duoyun-ui/src/patterns/form.ts b/packages/duoyun-ui/src/patterns/form.ts index ca53d8a0..9de3815e 100644 --- a/packages/duoyun-ui/src/patterns/form.ts +++ b/packages/duoyun-ui/src/patterns/form.ts @@ -1,6 +1,7 @@ import { html, GemElement, TemplateResult } from '@mantou/gem/lib/element'; import { adoptedStyle, customElement, property, refobject, RefObject } from '@mantou/gem/lib/decorators'; import { StyleObject, createCSSSheet, css, styleMap } from '@mantou/gem/lib/utils'; +import { history } from '@mantou/gem/lib/history'; import { icons } from '../lib/icons'; import { blockContainer } from '../lib/styles'; @@ -165,12 +166,21 @@ export class DyPatFormElement> extends GemElement valid = () => this.formRef.element!.valid(); } -type CreateFormOptions = { type?: 'modal' | 'drawer'; style?: StyleObject } & ModalOptions & +type CreateFormOptions = { + type?: 'modal' | 'drawer'; + style?: StyleObject; + query?: [string, any]; +} & ModalOptions & ModalOpenOptions & Pick, 'formItems' | 'data'>; export function createForm>(options: CreateFormOptions) { const containerType = options.type === 'modal' ? Modal : Drawer; + const { query } = history.getParams(); + if (options.query) { + query.setAny(options.query[0], options.query[1]); + history.replace({ query }); + } return containerType .open>({ header: options.header, @@ -192,5 +202,11 @@ export function createForm>(options: CreateFormOptio .then((ele) => ele.state) .catch((ele) => { throw ele.state; + }) + .finally(() => { + if (options.query) { + query.delete(options.query[0]); + history.replace({ query }); + } }); } diff --git a/packages/duoyun-ui/src/patterns/table.ts b/packages/duoyun-ui/src/patterns/table.ts index 90eb10ad..3c2657b6 100644 --- a/packages/duoyun-ui/src/patterns/table.ts +++ b/packages/duoyun-ui/src/patterns/table.ts @@ -30,7 +30,7 @@ import { } from '../lib/utils'; import type { Column, ItemContextMenuEventDetail, DuoyunTableElement } from '../elements/table'; import { blockContainer } from '../lib/styles'; -import { findScrollContainer } from '../lib/element'; +import { findRanges, findScrollContainer } from '../lib/element'; import { Time, formatDuration } from '../lib/time'; import type { DuoyunButtonElement } from '../elements/button'; import { locale } from '../lib/locale'; @@ -38,6 +38,7 @@ import { icons } from '../lib/icons'; import { isNotBoolean } from '../lib/types'; import { hotkeys } from '../lib/hotkeys'; import { PaginationStore } from '../helper/store'; +import { DuoyunRouteElement } from '../elements/route'; import { locationStore } from './console'; import type { FilterableOptions, FilterableType } from './filter-form'; @@ -48,6 +49,7 @@ import '../elements/tag'; import '../elements/table'; import '../elements/pagination'; import '../elements/scroll-box'; +import '../elements/loading'; import './filter-form'; @@ -58,11 +60,12 @@ export type FilterableColumn = Column & { }; }; -export const queryKeys = { - PAGINATION_PAGE: 'page', - PAGINATION_SIZE: 'size', - SEARCH: 'search', - FILTERS: 'filters', +// 不能和外部冲突 +const queryKeys = { + PAGINATION_PAGE: '_dy_page', + PAGINATION_SIZE: '_dy_size', + SEARCH: '_dy_search', + FILTERS: '_dy_filters', }; type Filter = { @@ -73,15 +76,20 @@ type Filter = { type State = { selection: any[]; + search: string; + filters: Filter[]; +}; +type LocationStore = Store>; + +export type FetchEventDetail = LocationStore & { page: number; size: number; search: string; filters: Filter[]; + searchAndFilterKey: string; }; -export type FetchEventDetail = State; - const style = createCSSSheet(css` .searchbar { display: flex; @@ -105,6 +113,11 @@ const style = createCSSSheet(css` .pagination { margin-block-start: ${theme.gridGutter}; } + .updating { + position: fixed; + right: 1rem; + bottom: 1rem; + } `); /** @@ -113,7 +126,6 @@ const style = createCSSSheet(css` @customElement('dy-pat-table') @adoptedStyle(style) @adoptedStyle(blockContainer) -// 只有使用 dy-pat-console 才起作用 @connectStore(locationStore) export class DyPatTableElement extends GemElement { @refobject tableRef: RefObject>; @@ -128,6 +140,9 @@ export class DyPatTableElement extends GemElement { @property data?: T[] | (T | undefined)[]; @property paginationStore?: Store>; + // 如果不在 dy-pat-console 中,则需要提供 `locationStore` + @property locationStore?: LocationStore; + @property rowKey?: string | string[]; @property getRowStyle?: (record: T) => Partial; @property getSelectedActions?: (selections: any[]) => ContextMenuItem[]; @@ -148,7 +163,7 @@ export class DyPatTableElement extends GemElement { | 'addAllSelection' | 'removeAllSelection' | ComparerType, - ) => string = (e) => e.replace(/[A-Z]/g, ' $1').replace(/^\w/, ($1) => $1.toUpperCase()); + ) => string = (e) => e.replace(/([A-Z])/g, ' $1').replace(/^\w/, ($1) => $1.toUpperCase()); get #defaultPagesize() { return this.pagesize || this.sizes?.[0] || 20; @@ -158,47 +173,30 @@ export class DyPatTableElement extends GemElement { return 1; } - state: State = (() => { - const p = history.getParams(); - const page = Number(p.query.get(queryKeys.PAGINATION_PAGE)) || this.#defaultPage; - const size = Number(p.query.get(queryKeys.PAGINATION_SIZE)) || this.#defaultPagesize; - const search = p.query.get(queryKeys.SEARCH) || ''; - const filters = p.query.getAnyAll(queryKeys.FILTERS); - return { search, filters, selection: [], page, size }; - })(); + get #page() { + return Number(history.getParams().query.get(queryKeys.PAGINATION_PAGE)) || this.#defaultPage; + } - #data?: (T | undefined)[] = []; + get #size() { + return Number(history.getParams().query.get(queryKeys.PAGINATION_SIZE)) || this.#defaultPagesize; + } - #getRanges = (root: Node, text: string) => { - const reg = new RegExp([...text].map((c) => `\\u{${c.codePointAt(0)!.toString(16)}}`).join(''), 'gui'); - const ranges: Range[] = []; - const nodes: Node[] = [root]; - while (!!nodes.length) { - const node = nodes.pop()!; - switch (node.nodeType) { - case Node.TEXT_NODE: - const matched = node.nodeValue?.matchAll(reg); - if (matched) { - for (const arr of matched) { - if (arr.index !== undefined) { - const range = new Range(); - range.setStart(node, arr.index); - range.setEnd(node, arr.index + text.length); - ranges.push(range); - } - } - } - break; - case Node.ELEMENT_NODE: - if ((node as Element).shadowRoot) nodes.push((node as Element).shadowRoot as Node); - break; - } - if (node.childNodes[0]) nodes.push(node.childNodes[0]); - if (node.nextSibling) nodes.push(node.nextSibling); - } - return ranges; + get #search() { + return history.getParams().query.get(queryKeys.SEARCH) || ''; + } + + get #filters() { + return history.getParams().query.getAnyAll(queryKeys.FILTERS); + } + + state: State = { + selection: [], + search: this.#search, + filters: this.#filters, }; + #data?: (T | undefined)[] = []; + #onSelect = (evt: CustomEvent) => this.setState({ selection: evt.detail }); #onItemContextMenu = (evt: CustomEvent>) => { @@ -223,7 +221,7 @@ export class DyPatTableElement extends GemElement { }, !this.paginationStore && { text: this.getText('addAllSelection'), - handle: () => table.appendSelection(this.data || []), + handle: () => table.appendSelection(this.#data || []), }, ].filter(isNotBoolean); ContextMenu.open( @@ -254,22 +252,26 @@ export class DyPatTableElement extends GemElement { }; #getPageData = () => { - const { query } = history.getParams(); - const page = Number(query.get(queryKeys.PAGINATION_PAGE)) || this.#defaultPage; - const size = Number(query.get(queryKeys.PAGINATION_SIZE)) || this.#defaultPagesize; + const page = this.#page; + const size = this.#size; const total = this.paginationStore ? this.paginationStore.total : this.#data ? Math.ceil(this.#data.length / size) : 0; - const data = this.paginationStore - ? this.paginationStore.pagination[page]?.ids?.map((id) => this.paginationStore!.items[id]) + const pageData = this.paginationStore?.pagination[page]; + + const data = pageData + ? pageData?.ids?.map((id) => this.paginationStore!.items[id]) : this.#data?.slice((page - 1) * size, page * size); - return { total, data, size, page }; + + const updating = data && pageData?.loading; + + return { total, data, size, page, updating }; }; - #createChangeFunction = (key: string, getDefaultValue: () => number, updateState: (v: number) => void) => { + #createChangeFunction = (key: string, getDefaultValue: () => number) => { return ({ detail }: CustomEvent) => { const p = history.getParams(); const query = new QueryString(p.query); @@ -278,23 +280,13 @@ export class DyPatTableElement extends GemElement { } else { query.set(key, String(detail)); } - updateState(detail); history.push({ ...p, query }); findScrollContainer(this)?.scrollTo(0, 0); - this.fetch(this.state); }; }; - #onPageChange = this.#createChangeFunction( - queryKeys.PAGINATION_PAGE, - () => this.#defaultPage, - (v) => this.setState({ page: v }), - ); - #onSizeChange = this.#createChangeFunction( - queryKeys.PAGINATION_SIZE, - () => this.#defaultPagesize, - (v) => this.setState({ size: v }), - ); + #onPageChange = this.#createChangeFunction(queryKeys.PAGINATION_PAGE, () => this.#defaultPage); + #onSizeChange = this.#createChangeFunction(queryKeys.PAGINATION_SIZE, () => this.#defaultPagesize); #changeQuery = () => { const p = history.getParams(); @@ -303,9 +295,7 @@ export class DyPatTableElement extends GemElement { query.setAny(queryKeys.FILTERS, this.state.filters); query.delete(queryKeys.PAGINATION_PAGE); query.delete(queryKeys.PAGINATION_SIZE); - this.setState({ page: this.#defaultPage, size: this.#defaultPagesize }); history.replace({ ...p, query, hash: '' }); - this.fetch(this.state); }; #changeQueryThrottle = throttle(this.#changeQuery, 120); @@ -372,10 +362,6 @@ export class DyPatTableElement extends GemElement { }; willMount = () => { - this.effect( - () => this.paginationStore && connect(this.paginationStore, this.update), - () => [this.paginationStore], - ); // 显示正确的过滤器文本 this.memo( () => { @@ -391,13 +377,13 @@ export class DyPatTableElement extends GemElement { }, () => [this.columns], ); - // 搜索和过滤,如果是 lazy,则由用户处理 + // 搜索和过滤数据,如果服务端分页,则跳过 this.memo( - ([query]) => { + () => { this.#data = this.data; if (this.paginationStore) return; if (!this.#data) return; - const search = query.get(queryKeys.SEARCH); + const { search, filters } = this.state; if (search) { this.#data = this.#data.filter((e) => { if (!e) return true; @@ -416,7 +402,6 @@ export class DyPatTableElement extends GemElement { return isIncludesString(str, search); }); } - const filters = query.getAnyAll(queryKeys.FILTERS) as Filter[]; filters.forEach(() => { this.#data = this.#data?.filter((e) => { return filters.every(({ field, comparer: comparerType, value }) => { @@ -435,16 +420,54 @@ export class DyPatTableElement extends GemElement { }); }); }, - () => [locationStore.query, this.columns, this.data] as const, + () => [(this.locationStore || locationStore).query, this.columns, this.data], ); }; mounted = () => { - this.fetch(this.state); + this.effect( + () => this.paginationStore && connect(this.paginationStore, this.update), + () => [this.paginationStore], + ); + this.effect( + () => this.locationStore && connect(this.locationStore, this.update), + () => [this.locationStore], + ); + this.effect( + () => { + const { search, filters } = this.state; + this.fetch({ + search, + filters, + page: this.#page, + size: this.#size, + ...locationStore, + ...this.locationStore, + searchAndFilterKey: + search || filters.length + ? `${search}-${filters + .sort((a, b) => (a.field > b.field ? 1 : 0)) + .map(({ field, comparer, value }) => `${field}-${comparer}-${value}`) + .join()}` + : '', + }); + }, + () => { + const { path, query } = this.locationStore || locationStore; + return [ + query.get(queryKeys.PAGINATION_PAGE), + query.get(queryKeys.PAGINATION_SIZE), + query.get(queryKeys.FILTERS), + query.get(queryKeys.SEARCH), + path, + ]; + }, + ); // 高亮搜索词 this.effect( - async ([search]) => { + async () => { await sleep(1); + const { search } = this.state; const Highlight = (window as any).Highlight; const highlights = (CSS as any).highlights; if (!Highlight || !highlights) return; @@ -456,16 +479,17 @@ export class DyPatTableElement extends GemElement { const highlight = new Highlight(); splitString(search).forEach((s) => { - this.#getRanges(tbody, s).forEach((range) => highlight.add(range)); + findRanges(tbody, s).forEach((range) => highlight.add(range)); }); highlights.set('search', highlight); }, - () => [this.state.search], + // search 进行了节流,所以是依赖 query + () => [this.paginationStore ? this.paginationStore.pagination[this.#page]?.ids : this.#search], ); }; render = () => { - const { data, page, size, total } = this.#getPageData(); + const { data, page, size, total, updating } = this.#getPageData(); return html`