Skip to content

Commit

Permalink
[duoyun-ui] Add helper/store
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed Jan 22, 2024
1 parent 28c46c0 commit 28c5de9
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 68 deletions.
123 changes: 100 additions & 23 deletions packages/duoyun-ui/docs/zh/30-blog/200-crud.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ DuoyunUI 的[模式元素](../01-guide/55-pattern.md)和帮助模块能让你快

- `<dy-pat-console>` 创建 App 基本布局
- `<dy-pat-table>` 创建表格页面
- `helper/store` 创建分页数据管理器
- `helper/error` 显示错误信息

<gbp-media src="/preview.png"></gbp-media>
Expand All @@ -14,14 +15,14 @@ DuoyunUI 的[模式元素](../01-guide/55-pattern.md)和帮助模块能让你快

<gbp-code-group>

```js Gem
```ts Gem
import { render, html } from '@mantou/gem';
import 'duoyun-ui/patterns/console';

render(html`<dy-pat-console></dy-pat-console>`, document.body);
```

```js React
```tsx React
import { createRoot } from 'react-dom/client';
import DyPatConsole from 'duoyun-ui/react/DyPatConsole';

Expand All @@ -37,7 +38,7 @@ createRoot(document.body).render(<DyPatConsole />);

<gbp-code-group>

```js Gem
```ts Gem
import { html } from '@mantou/gem';
import type { Routes, NavItems } from 'duoyun-ui/patterns/console';

Expand All @@ -54,7 +55,9 @@ const routes = {
title: 'Item Page',
async getContent(params) {
// import('./item');
return html`<console-page-item>${JSON.stringify(params)}</console-page-item>`;
return html`
<console-page-item>${JSON.stringify(params)}</console-page-item>
`;
},
},
} satisfies Routes;
Expand Down Expand Up @@ -105,7 +108,7 @@ const navItems: NavItems = [

之后,可以指定用户信息用来标识用户,还可以定义一些全局命令,比如切换语言、退出登录:

```js
```ts
import { Toast } from 'duoyun-ui/elements/toast';
import type { ContextMenus, UserInfo } from 'duoyun-ui/patterns/console';

Expand Down Expand Up @@ -135,7 +138,7 @@ const userInfo: UserInfo = {

<gbp-code-group>

```js Gem
```ts Gem
render(
html`
<dy-pat-console
Expand Down Expand Up @@ -176,7 +179,7 @@ createRoot(document.body).render(

<gbp-code-group>

```js Gem
```ts Gem
import { html, GemElement, connectStore, customElement } from '@mantou/gem';
import { locationStore } from 'duoyun-ui/patterns/console';
import 'duoyun-ui/patterns/table';
Expand Down Expand Up @@ -242,7 +245,7 @@ export function Item() {

<gbp-code-group>

```js Gem
```ts Gem
import { html, GemElement, connectStore, customElement } from '@mantou/gem';
import { get } from '@mantou/gem/helper/request';
import { locationStore } from 'duoyun-ui/patterns/console';
Expand All @@ -252,13 +255,13 @@ import 'duoyun-ui/patterns/table';
@customElement('console-page-item')
@connectStore(locationStore)
export class ConsolePageItemElement extends GemElement {
state = {}
state = {};

#columns: FilterableColumn[] = [
{
title: 'No',
dataIndex: 'id',
}
},
];

mounted = async () => {
Expand All @@ -267,7 +270,13 @@ export class ConsolePageItemElement extends GemElement {
};

render = () => {
return html`<dy-pat-table filterable .columns=${this.#columns} .data=${this.state.data}></dy-pat-table>`;
return html`
<dy-pat-table
filterable
.columns=${this.#columns}
.data=${this.state.data}
></dy-pat-table>
`;
};
}
```
Expand Down Expand Up @@ -306,7 +315,7 @@ export function Item() {

只需添加带有 `getActions` 的列:

```js
```ts
import { ContextMenu } from 'duoyun-ui/elements/contextmenu';

const columns: FilterableColumn[] = [
Expand All @@ -317,14 +326,17 @@ const columns: FilterableColumn[] = [
{
text: 'Edit',
handle: () => {
onUpdate(r)
onUpdate(r);
},
},
{
text: 'Delete',
danger: true,
handle: async () => {
await ContextMenu.confirm(`Confirm delete ${r.username}?`, { activeElement, danger: true });
await ContextMenu.confirm(`Confirm delete ${r.username}?`, {
activeElement,
danger: true,
});
console.log('Delete: ', r);
},
},
Expand All @@ -337,7 +349,7 @@ const columns: FilterableColumn[] = [

首先需要像定义表格一样定义表单:

```js
```ts
import type { FormItem } from 'duoyun-ui/patterns/form';

const formItems: FormItem[] = [
Expand All @@ -346,13 +358,13 @@ const formItems: FormItem[] = [
field: 'username',
label: 'Username',
required: true,
}
]
},
];
```

接着实现 `onCreate``onUpdate`,并在页面中添加 `Create` 按钮:

```js
```ts
import { createForm } from 'duoyun-ui/patterns/form';

function onUpdate(r) {
Expand Down Expand Up @@ -385,11 +397,11 @@ function onCreate() {

<gbp-code-group>

```js Gem
```ts Gem
// ...

html`
<dy-pat-table filterable .data=${this.state.data} .columns=${this.#columns}>
<dy-pat-table filterable .columns=${this.#columns} .data=${this.state.data}>
<dy-button @click=${onCreate}>Create</dy-button>
</dy-pat-table>
`;
Expand All @@ -405,17 +417,82 @@ html`

</gbp-code-group>

## 步骤3:服务端分页(可选)

到目前为止,应用虽然有分页、搜索、过滤功能,但这都是通过客户端实现的,意味着需要一次性为 `<dy-pat-table>` 提供所有数据。
在真实生产环境中,通常由服务端进行分页、搜索、过滤,只需要小的修改即可实现:

<gbp-code-group>

```ts Gem 7-8
// ...

html`
<dy-pat-table
filterable
.columns=${this.#columns}
.paginationStore=${store}
@fetch=${this.#onFetch}
>
<dy-button @click=${onCreate}>Create</dy-button>
</dy-pat-table>
`;
```

```tsx React 6-7
// ...

<DyPatTable
filterable={true}
columns={columns}
paginationStore={store}
onfetch={onFetch}
>
<DyButton onClick={onCreate}>Create</DyButton>
</DyPatTable>
```

</gbp-code-group>

其中,`store` 是使用 `createPaginationStore` 创建的分页数据:

```ts
import { Time } from 'duoyun-ui/lib/time';
import { PaginationReq, createPaginationStore } from 'duoyun-ui/helper/store';
import type { ChangeEventDetail } from 'duoyun-ui/patterns/table';

const { store, updatePage } = createPaginationStore({
storageKey: 'users',
cacheItems: true,
pageContainItem: true,
});

// 模拟真实 API
const fetchList = (args: PaginationReq) => {
return get(`https://jsonplaceholder.typicode.com/users`).then((list) => {
list.forEach((e, i) => {
e.updated = new Time().subtract(i + 1, 'd').getTime();
e.id += 10 * (args.page - 1);
});
return { list, count: list.length * 3 };
});
};

const onFetch = ({ detail }: CustomEvent<ChangeEventDetail>) => {
updatePage(fetchList, detail);
};
```

> [!TIP] `<dy-pat-table>` 还支持:
>
> - 使用 `expandedRowRender` 展开行,`@expand` 获取展开事件
> - 使用 `lazy``@change` 来处理分页信息(默认认为 `data` 是所有数据)
> - 使用 `selectable` 让表格可以框选,使用 `getSelectedActions` 添加选择项命令
## 步骤3:处理错误
## 步骤4:处理错误

在主文件开头引入 `helper/error`,它通过 `Toast` 显示错误信息,它也能显示未处理但被拒绝的 Promise:

```js
```ts
import 'duoyun-ui/helper/error';
```

Expand Down
104 changes: 104 additions & 0 deletions packages/duoyun-ui/src/helper/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { UseCacheStoreOptions, useCacheStore } from '../lib/utils';

export type PaginationReq = {
page: number;
size: number;
};

type PaginationRes<T> =
| {
list: T[];
// 总项目数
count: number;
}
| {
list: T[];
// 总页数
total: number;
};

type PageInit = { ids?: string[]; loading?: boolean };

export type PaginationStore<T> = {
// 总页数
total: number;
// 每页数据,只存 ID
pagination: Partial<{ [page: number]: PageInit }>;
// 每项数据
items: Partial<{ [id: string]: T }>;
// 正在加载,避免重复请求
loader: Partial<Record<string, Promise<T>>>;
};

type PaginationStoreOptions<T> = {
// 缓存 key
storageKey: string;
// 指定 item 的唯一 key
idKey?: keyof T;
// 页面数据是否包含 item,如果包含,则不需要调用额外的 updateItem
pageContainItem?: boolean;
// 是否缓存 Item,如果不缓存,store.items 中的内容可能为空
cacheItems?: boolean;
} & UseCacheStoreOptions<T>;

export function createPaginationStore<T extends Record<string, any>>(options: PaginationStoreOptions<T>) {
const { idKey = 'id', storageKey, cacheItems, pageContainItem, ...rest } = options;

const cacheExcludeKeys: (keyof PaginationStore<T>)[] = ['loader'];
if (cacheItems) cacheExcludeKeys.push('items');

const initStore: PaginationStore<T> = {
total: 0,
pagination: {},
items: {},
loader: {},
};
const [store, update, saveStore] = useCacheStore<PaginationStore<T>>(storageKey, initStore, {
...rest,
cacheExcludeKeys,
});

const changePage = (page: number, content: Partial<PageInit>) => {
store.pagination[page] = { ...store.pagination[page], ...content };
update();
};

const updatePage = async (request: (args: PaginationReq) => Promise<PaginationRes<any>>, args: PaginationReq) => {
changePage(args.page, { loading: true });
try {
const result = await request(args);
changePage(args.page, { ids: result.list.map((e) => e[idKey]) });

if (pageContainItem) {
result.list.forEach((e) => (store.items[e[idKey]] = e));
}

update({ total: 'total' in result ? result.total : Math.ceil(result.count / args.size) });
} finally {
changePage(args.page, { loading: false });
}
};

const updateItem = async (request: (id: string) => Promise<T>, id: string) => {
if (store.loader[id]) return;
const loader = request(id);
store.loader[id] = loader;
update();
try {
const item = await loader;
store.items[item[idKey]] = item;
update();
} finally {
delete store.loader[id];
update();
}
};

return {
store,
update,
saveStore,
updateItem,
updatePage,
};
}
Loading

0 comments on commit 28c5de9

Please sign in to comment.