- 建立 pnpm workspace + Turborepo 单仓、ESLint 9/Prettier/commitlint/husky 门禁 - NestJS + Fastify API:统一错误体、trace_id、Zod 管道、/health 真实依赖探活、OpenAPI 3.1 契约 - Prisma 基线:outbox_events/idempotency_keys/system_configs 与首版迁移、幂等种子 - shared 契约包:错误码、权限点、ULID、游标分页、命名/时间工具 - ui 包与 Web 壳层:Semi + Remix 图标统一出口、Cloud Blue 视觉、登录/工作台/错误页 - worker 骨架、Playwright 冒烟、Gitea CI、OpenAPI 快照与 oasdiff 回退脚本 - 补充 A0 实施报告与本地开发说明
This commit is contained in:
1 parent
9c3ab9768a
commit
04f49690f4
155 files changed
+18402
-28
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import { Table } from '@douyinfe/semi-ui-19';
|
||||
import type { TableProps } from '@douyinfe/semi-ui-19/lib/es/table/interface';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { PageEmpty } from './PageEmpty.js';
|
||||
|
||||
export interface DataTableProps<T extends Record<string, unknown>> {
|
||||
columns: NonNullable<TableProps<T>['columns']>;
|
||||
dataSource: T[];
|
||||
loading?: boolean;
|
||||
rowKey?: TableProps<T>['rowKey'];
|
||||
pagination?: TableProps<T>['pagination'];
|
||||
empty?: ReactNode;
|
||||
size?: TableProps<T>['size'];
|
||||
scroll?: TableProps<T>['scroll'];
|
||||
className?: string;
|
||||
onRow?: TableProps<T>['onRow'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表表格统一封装:加载态 + 空态 + 行 key 约定。
|
||||
* 业务页只负责列定义与数据,不重复写 Empty / loading。
|
||||
*/
|
||||
export function DataTable<T extends Record<string, unknown>>({
|
||||
columns,
|
||||
dataSource,
|
||||
loading = false,
|
||||
rowKey = 'id',
|
||||
pagination = false,
|
||||
empty,
|
||||
size = 'middle',
|
||||
scroll,
|
||||
className,
|
||||
onRow,
|
||||
}: DataTableProps<T>): ReactNode {
|
||||
return (
|
||||
<Table<T>
|
||||
className={`lx-data-table${className === undefined ? '' : ` ${className}`}`}
|
||||
columns={columns}
|
||||
dataSource={dataSource}
|
||||
loading={loading}
|
||||
rowKey={rowKey}
|
||||
pagination={pagination}
|
||||
size={size}
|
||||
scroll={scroll}
|
||||
onRow={onRow}
|
||||
empty={empty ?? <PageEmpty />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Button } from '@douyinfe/semi-ui-19';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { IconError, IconRefresh } from '../icons.js';
|
||||
|
||||
export interface ErrorStateProps {
|
||||
title?: string;
|
||||
message?: string;
|
||||
/** 服务端返回的 trace_id;500 类错误必须展示(工程规范 04) */
|
||||
traceId?: string;
|
||||
onRetry?: () => void;
|
||||
retryText?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 统一错误态:说明 + trace_id + 重试 */
|
||||
export function ErrorState({
|
||||
title = '加载失败',
|
||||
message = '请稍后重试;若持续失败,请把 trace_id 提供给管理员。',
|
||||
traceId,
|
||||
onRetry,
|
||||
retryText = '重新加载',
|
||||
className,
|
||||
}: ErrorStateProps): ReactNode {
|
||||
return (
|
||||
<div className={`lx-error-state${className === undefined ? '' : ` ${className}`}`}>
|
||||
<span className="lx-error-state__icon" aria-hidden="true">
|
||||
<IconError size={32} />
|
||||
</span>
|
||||
<h3 className="lx-error-state__title">{title}</h3>
|
||||
<p className="lx-error-state__message">{message}</p>
|
||||
{traceId === undefined || traceId.length === 0 ? null : (
|
||||
<p className="lx-error-state__trace">trace_id: {traceId}</p>
|
||||
)}
|
||||
{onRetry === undefined ? null : (
|
||||
<Button icon={<IconRefresh />} onClick={onRetry} className="lx-error-state__retry">
|
||||
{retryText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Modal } from '@douyinfe/semi-ui-19';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface FormModalProps {
|
||||
visible: boolean;
|
||||
title: ReactNode;
|
||||
children: ReactNode;
|
||||
onCancel: () => void;
|
||||
onOk: () => void;
|
||||
confirmLoading?: boolean;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
width?: number | string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 表单弹窗统一封装:确定 / 取消 / 提交中状态 */
|
||||
export function FormModal({
|
||||
visible,
|
||||
title,
|
||||
children,
|
||||
onCancel,
|
||||
onOk,
|
||||
confirmLoading = false,
|
||||
okText = '确定',
|
||||
cancelText = '取消',
|
||||
width = 560,
|
||||
className,
|
||||
}: FormModalProps): ReactNode {
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={title}
|
||||
onCancel={onCancel}
|
||||
onOk={onOk}
|
||||
confirmLoading={confirmLoading}
|
||||
okText={okText}
|
||||
cancelText={cancelText}
|
||||
width={width}
|
||||
className={`lx-form-modal${className === undefined ? '' : ` ${className}`}`}
|
||||
closeOnEsc
|
||||
maskClosable={false}
|
||||
>
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Empty } from '@douyinfe/semi-ui-19';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { IconAsset } from '../icons.js';
|
||||
|
||||
export interface PageEmptyProps {
|
||||
title?: ReactNode;
|
||||
description?: ReactNode;
|
||||
action?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 统一空态:标题 + 说明 + 可选主操作 */
|
||||
export function PageEmpty({
|
||||
title = '暂无数据',
|
||||
description,
|
||||
action,
|
||||
className,
|
||||
}: PageEmptyProps): ReactNode {
|
||||
return (
|
||||
<div className={`lx-empty${className === undefined ? '' : ` ${className}`}`}>
|
||||
<Empty image={<IconAsset size={40} />} title={title} description={description}>
|
||||
{action === undefined ? null : <div className="lx-empty__action">{action}</div>}
|
||||
</Empty>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Skeleton } from '@douyinfe/semi-ui-19';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface PageSkeletonProps {
|
||||
loading?: boolean;
|
||||
rows?: number;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 统一加载骨架:页面级加载用段落占位,避免布局跳动 */
|
||||
export function PageSkeleton({
|
||||
loading = true,
|
||||
rows = 6,
|
||||
children,
|
||||
className,
|
||||
}: PageSkeletonProps): ReactNode {
|
||||
return (
|
||||
<Skeleton
|
||||
loading={loading}
|
||||
placeholder={<Skeleton.Paragraph rows={rows} />}
|
||||
className={`lx-page-skeleton${className === undefined ? '' : ` ${className}`}`}
|
||||
>
|
||||
{children}
|
||||
</Skeleton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Button, Form, Space } from '@douyinfe/semi-ui-19';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { IconRefresh, IconSearch } from '../icons.js';
|
||||
|
||||
export interface QueryFormProps {
|
||||
children: ReactNode;
|
||||
/** 查询动作;由页面自行发起请求 */
|
||||
onSubmit: () => void;
|
||||
onReset?: () => void;
|
||||
loading?: boolean;
|
||||
/** 额外的右侧操作(如「新建」) */
|
||||
extra?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** 列表页查询表单:字段一行内排列,查询 / 重置固位;筛选行不超过两行(设计约束) */
|
||||
export function QueryForm({
|
||||
children,
|
||||
onSubmit,
|
||||
onReset,
|
||||
loading = false,
|
||||
extra,
|
||||
className,
|
||||
}: QueryFormProps): ReactNode {
|
||||
return (
|
||||
<Form
|
||||
className={`lx-query-form${className === undefined ? '' : ` ${className}`}`}
|
||||
layout="horizontal"
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<div className="lx-query-form__fields">{children}</div>
|
||||
<Space className="lx-query-form__actions" spacing={8}>
|
||||
<Button
|
||||
htmlType="submit"
|
||||
theme="solid"
|
||||
type="primary"
|
||||
icon={<IconSearch />}
|
||||
loading={loading}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
icon={<IconRefresh />}
|
||||
onClick={() => {
|
||||
onReset?.();
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
{extra}
|
||||
</Space>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { DataTable } from './DataTable.js';
|
||||
export { ErrorState } from './ErrorState.js';
|
||||
export { FormModal } from './FormModal.js';
|
||||
export { PageEmpty } from './PageEmpty.js';
|
||||
export { PageSkeleton } from './PageSkeleton.js';
|
||||
export { QueryForm } from './QueryForm.js';
|
||||
Reference in new issue
Block a user