feat(a0): 完成工程骨架与契约基线
ci / 质量门禁 (push) Canceled after 0s

- 建立 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:
Stev_Wang committed 2026-11-02 17:01:50 +08:00
1 parent 9c3ab9768a
commit 04f49690f4
155 files changed
+18402 -28

No files matched your search

+50
View File
@@ -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 />}
/>
);
}
+42
View File
@@ -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>
);
}
+47
View File
@@ -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>
);
}
+27
View File
@@ -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>
);
}
+57
View File
@@ -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>
);
}
+6
View File
@@ -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';