Viewer
维护期(已弃用)
@ahoo-wang/fetcher-viewer 已进入维护期(弃用),仅维护现有功能,不再新增功能。数据视图能力的后续演进由 @ahoo-wang/fetcher-view-engine 承担,新项目请使用 View Engine。本页保留供存量项目维护参考;两者模型与 API 不同,迁移需要适配。
Viewer 提供 Ant Design 过滤、表格和保存视图 UI。先根据谁拥有行数据和保存视图选择组件。本地表格不要求 Wow 后端;FetcherViewer 要求既定的远端协议。
选择入口
| 入口 | 组件负责 | 应用负责 |
|---|---|---|
| View | 单视图过滤/表格/分页交互 | dataSource: {list,total}、数据加载和持久化 |
| Viewer | 视图集合、切换和活动视图编辑状态 | definition、初始视图、onLoadData、增改删持久化及成功回调 |
| FetcherViewer | 定义/视图读取、Wow 命令与分页数据请求 | 兼容后端、默认 Fetcher/认证、租户/所有者身份和授权 |
View 与 Viewer 不替你过滤或分页输入数组;回调请求新数据,应用提供结果行与总数。保存成功回调确认的是应用的持久化。FetcherViewer 为创建/更新增加版本确认;删除和普通行查询有不同边界。
完整安装前提
sh
pnpm add @ahoo-wang/fetcher @ahoo-wang/fetcher-cosec @ahoo-wang/fetcher-decorator @ahoo-wang/fetcher-eventbus @ahoo-wang/fetcher-eventstream @ahoo-wang/fetcher-openapi @ahoo-wang/fetcher-react @ahoo-wang/fetcher-storage @ahoo-wang/fetcher-viewer @ahoo-wang/fetcher-wow @ant-design/icons antd dayjs react react-dom本参考针对 5.0.0。库包声明 Node >=18.20.8;仓库开发要求 Node >=20.20.2、pnpm 10.34.5。命令包含递归内部 peer,包括经 Wow/React/CoSec 引入的包;直接运行依赖自动安装。 外部 peer 范围为 React/ReactDOM ^19.2.8、antd ^6.6.3、@ant-design/icons ^6.3.4、dayjs ^1.11.23;即使不使用某项功能,仍是安装前提。消费者无需复制仓库的 React Compiler 工具链。
可运行核心示例
运行步骤、服务夹具与预期结果见接入指南.
tsx
/*
* Copyright [2021-present] [ahoo wang <ahoowang@qq.com> (https://github.com/Ahoo-Wang)].
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useCallback, useState } from 'react';
import { App } from 'antd';
import { FullscreenProvider } from '@ahoo-wang/fetcher-react';
import { Viewer } from '@ahoo-wang/fetcher-viewer';
import type { ViewDefinition, ViewState } from '@ahoo-wang/fetcher-viewer';
import { all, Operator, SortDirection } from '@ahoo-wang/fetcher-wow';
import type { Condition, FieldSort, PagedList } from '@ahoo-wang/fetcher-wow';
const users = [
{ id: 'u-ada', name: 'Ada', active: true },
{ id: 'u-lin', name: 'Lin', active: false },
{ id: 'u-grace', name: 'Grace', active: true },
{ id: 'u-zoe', name: 'Zoe', active: false },
];
type User = (typeof users)[number];
const definition: ViewDefinition = {
id: 'local-users',
name: 'Local users',
fields: [
{ name: 'id', label: 'ID', type: 'text', primaryKey: true },
{
name: 'name',
label: 'Name',
type: 'text',
primaryKey: false,
sorter: true,
},
{
name: 'active',
label: 'Active',
type: 'text',
primaryKey: false,
render: value => (value ? 'Yes' : 'No'),
},
],
availableFilters: [
{
label: 'User',
filters: [
{
key: 'active',
field: { name: 'active', label: 'Active' },
component: 'bool',
},
],
},
],
// Required metadata; this local application does not fetch these URLs.
dataUrl: '/local-users/paged',
countUrl: '/local-users/count',
};
const initialView: ViewState = {
id: 'all-users',
name: 'All users',
definitionId: definition.id,
type: 'PERSONAL',
source: 'SYSTEM',
isDefault: true,
filters: [
{ key: 'active', type: 'bool', field: { name: 'active', label: 'Active' } },
],
columns: definition.fields.map(field => ({
key: field.name,
name: field.name,
fixed: field.primaryKey,
hidden: false,
})),
tableSize: 'middle',
pageSize: 2,
condition: all(),
sorter: [],
};
function filterUsers(condition: Condition): User[] {
if (condition.operator === Operator.ALL) return [...users];
if (
condition.field === 'active' &&
(condition.operator === Operator.TRUE ||
condition.operator === Operator.FALSE)
) {
return users.filter(
user => user.active === (condition.operator === Operator.TRUE),
);
}
throw new Error('This example supports only the Active boolean filter.');
}
function queryUsers(
condition: Condition,
page: number,
size: number,
sorter: FieldSort[] = [],
): PagedList<User> {
const rows = filterUsers(condition);
if (
sorter.length > 1 ||
sorter.some(
sort =>
sort.field !== 'name' ||
![SortDirection.ASC, SortDirection.DESC].includes(sort.direction),
)
) {
throw new Error(
'This example supports only ascending/descending Name sorting.',
);
}
if (sorter.length) {
const direction = sorter[0].direction === SortDirection.ASC ? 1 : -1;
rows.sort(
(left, right) => left.name.localeCompare(right.name, 'en') * direction,
);
}
return {
list: rows.slice((page - 1) * size, page * size),
total: rows.length,
};
}
export function LocalViewer() {
const [savedViews, setSavedViews] = useState<ViewState[]>([initialView]);
const [data, setData] = useState(() => queryUsers(all(), 1, 2));
const [savedName, setSavedName] = useState('');
const [error, setError] = useState('');
const load = useCallback(
(
condition: Condition,
page: number,
size: number,
sorter?: FieldSort[],
) => {
try {
setData(queryUsers(condition, page, size, sorter));
setError('');
} catch (failure) {
setData({ list: [], total: 0 });
setError(failure instanceof Error ? failure.message : String(failure));
}
},
[],
);
return (
<App>
<FullscreenProvider>
<Viewer<User>
definition={definition}
defaultViews={savedViews}
defaultView={initialView}
dataSource={data}
enableRowSelection={false}
pagination={{ showSizeChanger: false }}
onLoadData={load}
onSwitchView={view =>
load(view.condition, 1, view.pageSize, view.sorter)
}
onGetRecordCount={async (_url, condition) =>
filterUsers(condition).length
}
onCreateView={(view, onSuccess) => {
const saved = { ...view, id: crypto.randomUUID() };
setSavedViews(current => [...current, saved]);
setSavedName(saved.name);
onSuccess?.(saved);
}}
onUpdateView={(view, onSuccess) => {
setSavedViews(current =>
current.map(saved => (saved.id === view.id ? view : saved)),
);
setSavedName(view.name);
onSuccess?.(view);
}}
onDeleteView={(view, onSuccess) => {
setSavedViews(current =>
current.filter(saved => saved.id !== view.id),
);
onSuccess?.(view);
}}
/>
{error && <p role="alert">{error}</p>}
<output aria-live="polite">{savedName && `Saved: ${savedName}`}</output>
</FullscreenProvider>
</App>
);
}