listpage_cli 0.0.286 → 0.0.292
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +15 -0
- package/bin/copy.js +4 -0
- package/bin/prompts.js +2 -0
- package/package.json +3 -2
- package/skills/listpage/SKILL.md +26 -0
- package/skills/listpage/api.md +163 -0
- package/skills/listpage/examples.md +243 -0
- package/templates/backend-template/.env.example +7 -0
- package/templates/backend-template/package.json.tmpl +5 -3
- package/templates/backend-template/prisma/schema.prisma +1 -2
- package/templates/backend-template/prisma.config.ts +9 -0
- package/templates/backend-template/src/modules/app.module.ts +7 -1
- package/templates/backend-template/src/modules/prisma/prisma.service.ts +19 -2
- package/templates/frontend-template/package.json.tmpl +3 -1
- package/templates/frontend-template/src/App.tsx.tmpl +61 -10
- package/templates/frontend-template/src/layouts/MainLayout.tsx +34 -0
- package/templates/frontend-template/src/router/menus.tsx +5 -10
- package/templates/package-app-template/package.json +1 -1
- package/templates/rush-template/docs/ListPage-AI/347/224/237/346/210/220/350/247/204/350/214/203.md +41 -0
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const fs_1 = require("fs");
|
|
7
8
|
const path_1 = __importDefault(require("path"));
|
|
8
9
|
const prompts_1 = require("./prompts");
|
|
9
10
|
const copy_1 = require("./copy");
|
|
@@ -52,6 +53,8 @@ async function main() {
|
|
|
52
53
|
return initCmd();
|
|
53
54
|
if (cmd === "sync-docs")
|
|
54
55
|
return syncDocsCmd();
|
|
56
|
+
if (cmd === "install-skill")
|
|
57
|
+
return installSkillCmd();
|
|
55
58
|
(0, prompts_1.printHelp)();
|
|
56
59
|
}
|
|
57
60
|
main();
|
|
@@ -65,3 +68,15 @@ async function syncDocsCmd() {
|
|
|
65
68
|
(0, copy_1.syncDirWithRename)(sourceDocs, destDocs);
|
|
66
69
|
console.log("已同步 .trae 和 docs 到执行目录的同级目录");
|
|
67
70
|
}
|
|
71
|
+
async function installSkillCmd() {
|
|
72
|
+
const args = process.argv.slice(2);
|
|
73
|
+
const skillName = args.find((a) => a !== "install-skill") || "test";
|
|
74
|
+
const sourceDir = path_1.default.join(__dirname, "..", "skills", skillName);
|
|
75
|
+
if (!(0, fs_1.existsSync)(sourceDir)) {
|
|
76
|
+
console.error(`技能 ${skillName} 不存在`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const targetDir = path_1.default.join(process.cwd(), ".cursor", "skills", skillName || "");
|
|
80
|
+
(0, copy_1.copySkillDir)(sourceDir, targetDir);
|
|
81
|
+
console.log(`已安装 ${skillName} 技能到 ${targetDir}`);
|
|
82
|
+
}
|
package/bin/copy.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.copyBackendTemplate = copyBackendTemplate;
|
|
|
10
10
|
exports.copyDeployScriptTemplate = copyDeployScriptTemplate;
|
|
11
11
|
exports.ensureDir = ensureDir;
|
|
12
12
|
exports.isDirEmpty = isDirEmpty;
|
|
13
|
+
exports.copySkillDir = copySkillDir;
|
|
13
14
|
exports.syncDirWithRename = syncDirWithRename;
|
|
14
15
|
const fs_1 = require("fs");
|
|
15
16
|
const path_1 = __importDefault(require("path"));
|
|
@@ -100,6 +101,9 @@ function compileTemplateContent(str, vars) {
|
|
|
100
101
|
function isDirEmpty(dir) {
|
|
101
102
|
return !(0, fs_1.existsSync)(dir) || (0, fs_1.readdirSync)(dir).length === 0;
|
|
102
103
|
}
|
|
104
|
+
function copySkillDir(sourceDir, targetDir) {
|
|
105
|
+
syncDirWithRename(sourceDir, targetDir);
|
|
106
|
+
}
|
|
103
107
|
function syncDirWithRename(source, destination) {
|
|
104
108
|
ensureDir(destination);
|
|
105
109
|
const items = (0, fs_1.readdirSync)(source);
|
package/bin/prompts.js
CHANGED
|
@@ -64,6 +64,8 @@ function printHelp() {
|
|
|
64
64
|
"说明: 进入中文引导式交互,按提示填写即可",
|
|
65
65
|
"用法: listpage_cli sync-docs",
|
|
66
66
|
"说明: 将模板中的 .trae 和 docs 同步到执行目录的同级目录",
|
|
67
|
+
"用法: listpage_cli install-skill [listpage] [--project]",
|
|
68
|
+
"说明: 安装 listpage 技能到 Cursor。默认安装到 ~/.cursor/skills/;加 --project 则安装到当前项目 .cursor/skills/",
|
|
67
69
|
].join("\n");
|
|
68
70
|
console.log(h);
|
|
69
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "listpage_cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.292",
|
|
4
4
|
"private": false,
|
|
5
5
|
"bin": {
|
|
6
6
|
"listpage_cli": "bin/cli.js"
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"bin",
|
|
17
|
-
"templates"
|
|
17
|
+
"templates",
|
|
18
|
+
"skills"
|
|
18
19
|
],
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"typescript": "^5.6.2",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: listpage
|
|
3
|
+
description: 使用 listpage-next 实现页面级列表/表格。Use ONLY when user explicitly requests to implement a page with listpage, e.g. 用 listpage 实现 xxx 页面, 使用 ListPage 做 xxx. Do NOT apply for generic list/table requests without explicit listpage mention.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ListPage 使用技能
|
|
7
|
+
|
|
8
|
+
示例与 API 见 [examples.md](examples.md)、[api.md](api.md)。
|
|
9
|
+
|
|
10
|
+
## 生成流程(7 步)
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Task Progress:
|
|
14
|
+
- [ ] 步骤 1:创建目录结构(简单页面单文件;复杂页面 config/ + components/)
|
|
15
|
+
- [ ] 步骤 2:定义 filters(FilterFormOption[],AntD 组件 + allowClear)
|
|
16
|
+
- [ ] 步骤 3:定义 columns (ListPageTableColumn[],优先 component: 'text'|'link'|'switch')
|
|
17
|
+
- [ ] 步骤 4:实现 request((params, filters) => Promise<PaginationData<T>>)
|
|
18
|
+
- [ ] 步骤 5:组装 index.tsx(initialValues、注入 request/filter/table/floats )
|
|
19
|
+
- [ ] 步骤 6:注册 floats(若有浮窗:floats=[{ key, render }],ctx.showFloat(key, record, true))
|
|
20
|
+
- [ ] 步骤 7:自检(rowKey、storageKey 唯一、浮窗 key 短横线小写)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 命名约定
|
|
24
|
+
|
|
25
|
+
- 浮窗 key:短横线小写(如 `create`、`edit-detail`)
|
|
26
|
+
- 文件:小驼峰(`columns.tsx`、`filters.tsx`)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# API(listpage-components)
|
|
2
|
+
|
|
3
|
+
基于 `listpage-components` 的 ListPage Props 及使用中涉及的类型。
|
|
4
|
+
|
|
5
|
+
## ListPageProps
|
|
6
|
+
|
|
7
|
+
| 属性 | 说明 | 类型 | 必填 |
|
|
8
|
+
| --- | --- | --- | --- |
|
|
9
|
+
| `title` | 页面标题 | `ReactNode` | 否 |
|
|
10
|
+
| `extra` | 标题右侧操作区 | `ReactNode \| (ctx: ListPageContextValue) => ReactNode` | 否 |
|
|
11
|
+
| `filter` | 筛选配置 | `FilterConfig \| FilterRenderConfig` | 否 |
|
|
12
|
+
| `toolbar` | 工具栏 | `ToolbarRenderConfig` | 否 |
|
|
13
|
+
| `table` | 表格配置 | `TableConfig \| TableRenderConfig` | 是 |
|
|
14
|
+
| `request` | 数据请求 | `ListPageRequest` | 是 |
|
|
15
|
+
| `floats` | 浮窗列表 | `FloatItem[]` | 否 |
|
|
16
|
+
| `initialValues` | 初始值 | `InitialValues` | 否 |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## InitialValues
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
type InitialValues = {
|
|
24
|
+
filterValues?: any; // 预填筛选
|
|
25
|
+
pageSize?: number; // 默认每页条数
|
|
26
|
+
currentPage?: number; // 默认当前页
|
|
27
|
+
};
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## filter(FilterConfig)
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
type FilterConfig = { items: FilterItem[] };
|
|
36
|
+
|
|
37
|
+
type FilterItem = {
|
|
38
|
+
name: string;
|
|
39
|
+
label?: ReactNode;
|
|
40
|
+
component?: ReactElement; // antd Input、Select 等
|
|
41
|
+
colSpan?: number;
|
|
42
|
+
formItemProps?: Omit<FormItemProps, 'children'>;
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
也可使用 `FilterRenderConfig`:`{ render: (props: FilterRenderProps) => ReactNode }` 自定义渲染。
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## toolbar(ToolbarRenderConfig)
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
type ToolbarRenderConfig = {
|
|
54
|
+
render: (props: ToolbarRenderProps) => ReactNode;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type ToolbarRenderProps = {
|
|
58
|
+
ctx: ListPageContextValue;
|
|
59
|
+
selection?: SelectionState; // { selectedRowKeys; selectedRows }
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## table(TableConfig)
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
type TableConfig = {
|
|
69
|
+
columns: ListPageColumnType[];
|
|
70
|
+
rowKey?: string;
|
|
71
|
+
rowSelection?: boolean | object;
|
|
72
|
+
title?: ReactNode; // 表格标题
|
|
73
|
+
extra?: ReactNode | (ctx: ListPageContextValue) => ReactNode;
|
|
74
|
+
rowTitleKey?: string; // 选中项 Tooltip 展示的字段
|
|
75
|
+
pagination?: PaginationProps; // antd 分页配置
|
|
76
|
+
[key: string]: any; // 透传至 antd Table
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## ListPageColumnType
|
|
83
|
+
|
|
84
|
+
继承 antd `ColumnType`,额外支持:
|
|
85
|
+
|
|
86
|
+
| 属性 | 说明 | 类型 |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| `renderer` | 内置渲染器 | `ColumnRendererType` |
|
|
89
|
+
| `rendererOptions` | 渲染器配置 | `ColumnRendererOptions[renderer]` |
|
|
90
|
+
| `component` | 自定义渲染(优先于 renderer) | `(props: ColumnComponentProps) => ReactNode` |
|
|
91
|
+
|
|
92
|
+
**ColumnComponentProps**:`{ ctx: ListPageContextValue; record; index; value }`
|
|
93
|
+
|
|
94
|
+
**ColumnRendererType**:`'text' | 'ellipsis' | 'tag' | 'badge' | 'date' | 'datetime' | 'number' | 'money' | 'percent' | 'boolean' | 'link' | 'enum' | 'copyable' | 'avatar' | 'image'`
|
|
95
|
+
|
|
96
|
+
**ColumnRendererOptions**(按 renderer 选配):
|
|
97
|
+
- `ellipsis`: `{ maxLength?: number }`
|
|
98
|
+
- `tag`: `{ colorMap?: Record<string, string>; options?: Array<{ value; label; color? }> }`
|
|
99
|
+
- `badge`: `{ colorMap }`
|
|
100
|
+
- `date` / `datetime`: `{ format?: string }`
|
|
101
|
+
- `number` / `money` / `percent`: `{ precision?: number }`,`money` 额外 `{ prefix?: string }`
|
|
102
|
+
- `boolean`: `{ labels?: [string, string] }`
|
|
103
|
+
- `link`: `{ onClick?: (record, ctx?) => void; href?: (record) => string }`
|
|
104
|
+
- `enum`: `{ options: Array<{ value; label }> }`
|
|
105
|
+
- `avatar`: `{ fallback?: string }`
|
|
106
|
+
- `image`: `{ width?: number; height?: number }`
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## request(ListPageRequest)
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
type ListPageRequest = (
|
|
114
|
+
pageParams: RequestParams,
|
|
115
|
+
filterValues: any,
|
|
116
|
+
sortParams: SortParams
|
|
117
|
+
) => Promise<{ list: any[]; total: number }>;
|
|
118
|
+
|
|
119
|
+
type RequestParams = { current: number; pageSize: number };
|
|
120
|
+
type SortParams = { field?: string; order?: 'ascend' | 'descend' | null };
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## floats(FloatItem)
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
type FloatItem = {
|
|
129
|
+
key: string;
|
|
130
|
+
render: (props: FloatComponentProps) => ReactNode;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
type FloatComponentProps<T = any> = {
|
|
134
|
+
record: T;
|
|
135
|
+
visible: boolean;
|
|
136
|
+
onClose: (code?: number) => void | (() => void);
|
|
137
|
+
// (code?: number) => void:onClose() 取消不刷新,onClose(1) 确认并刷新
|
|
138
|
+
};
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## ListPageContextValue
|
|
144
|
+
|
|
145
|
+
`extra`、`toolbar.render`、`table.extra`、列 `component` 中通过 `useListPageContext()` 获取。
|
|
146
|
+
|
|
147
|
+
| 属性 | 说明 |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `filters` | 当前筛选值 |
|
|
150
|
+
| `initialFilterValues` | 初始筛选值 |
|
|
151
|
+
| `pagination` | `{ current; pageSize; total }` |
|
|
152
|
+
| `dataSource` | 表格数据 |
|
|
153
|
+
| `loading` | 加载中 |
|
|
154
|
+
| `selection` | `{ selectedRowKeys; selectedRows }` |
|
|
155
|
+
| `sort` | `{ field?; order? }` |
|
|
156
|
+
| `refreshTable` | 刷新表格 |
|
|
157
|
+
| `submitFilters` | 提交筛选并刷新 |
|
|
158
|
+
| `resetFilters` | 重置筛选 |
|
|
159
|
+
| `updatePagination` | 更新分页 `(page, pageSize)` |
|
|
160
|
+
| `updateSort` | 更新排序 `(field?, order?)` |
|
|
161
|
+
| `showFloat` | 打开浮窗 `(key, record, onCloseCallback?: true \| (() => void))` |
|
|
162
|
+
| `hideFloat` | 关闭浮窗 |
|
|
163
|
+
| `onSelectionChange` | 选中变更 `(keys, rows)` |
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# ListPage 示例
|
|
2
|
+
|
|
3
|
+
## 1. 最小示例(单文件内联,listpage-components)
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { ListPage } from "listpage-components";
|
|
7
|
+
import { Button, Input, Select, Space } from "antd";
|
|
8
|
+
|
|
9
|
+
const filterItems = [
|
|
10
|
+
{ name: "name", label: "姓名", component: <Input placeholder="请输入" /> },
|
|
11
|
+
{
|
|
12
|
+
name: "status",
|
|
13
|
+
label: "状态",
|
|
14
|
+
component: (
|
|
15
|
+
<Select
|
|
16
|
+
allowClear
|
|
17
|
+
options={[
|
|
18
|
+
{ value: "enabled", label: "启用" },
|
|
19
|
+
{ value: "disabled", label: "停用" },
|
|
20
|
+
]}
|
|
21
|
+
/>
|
|
22
|
+
),
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const columns = [
|
|
27
|
+
{ title: "姓名", dataIndex: "name", key: "name", renderer: "text" },
|
|
28
|
+
{ title: "地址", dataIndex: "address", key: "address", ellipsis: true },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const request = async (
|
|
32
|
+
{ current, pageSize }: { current: number; pageSize: number },
|
|
33
|
+
filterValues: any,
|
|
34
|
+
_sortParams: { field?: string; order?: "ascend" | "descend" | null }
|
|
35
|
+
) => {
|
|
36
|
+
const all = Array.from({ length: 100 }).map((_, i) => ({
|
|
37
|
+
id: i + 1,
|
|
38
|
+
name: `Edward ${i + 1}`,
|
|
39
|
+
address: `Address ${i + 1}`,
|
|
40
|
+
}));
|
|
41
|
+
let list = all.filter(
|
|
42
|
+
(r) => !filterValues?.name || r.name.includes(filterValues.name)
|
|
43
|
+
);
|
|
44
|
+
const skip = (current - 1) * pageSize;
|
|
45
|
+
return { list: list.slice(skip, skip + pageSize), total: list.length };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default function Page() {
|
|
49
|
+
return (
|
|
50
|
+
<ListPage
|
|
51
|
+
title="用户列表"
|
|
52
|
+
extra={(ctx) => (
|
|
53
|
+
<Space>
|
|
54
|
+
<Button onClick={() => ctx.refreshTable()}>刷新</Button>
|
|
55
|
+
</Space>
|
|
56
|
+
)}
|
|
57
|
+
filter={{ items: filterItems }}
|
|
58
|
+
table={{ columns, rowKey: "id" }}
|
|
59
|
+
request={request}
|
|
60
|
+
initialValues={{ currentPage: 1, pageSize: 10 }}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 2. 复杂用法(内置渲染器、toolbar、多浮窗)
|
|
67
|
+
|
|
68
|
+
内置列渲染器:`text`、`ellipsis`、`tag`、`badge`、`date`、`datetime`、`number`、`money`、`percent`、`boolean`、`link`、`enum`、`copyable`、`avatar`、`image`。浮窗 `onClose(1)` 表示确认并刷新,`onClose()` 表示取消。
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { ListPage } from "listpage-components";
|
|
72
|
+
import { Button, Input, Select, Space, Drawer, Modal, message } from "antd";
|
|
73
|
+
|
|
74
|
+
const request = async (pageParams, filterValues) => {
|
|
75
|
+
const { current, pageSize } = pageParams;
|
|
76
|
+
// 模拟数据与筛选...
|
|
77
|
+
const list = [{ id: "1", name: "张三", dept: "研发", status: "启用", price: "100.00", createTime: "2025-01-01" }];
|
|
78
|
+
return { list, total: list.length };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const DetailModal = ({ record, visible, onClose }) => (
|
|
82
|
+
<Modal open={visible} title="详情" onCancel={onClose} footer={null}>
|
|
83
|
+
<p>姓名:{record?.name}</p>
|
|
84
|
+
</Modal>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const EditModal = ({ record, visible, onClose }) => (
|
|
88
|
+
<Modal open={visible} title="编辑" onCancel={onClose} onOk={() => { message.success("保存成功"); onClose(1); }}>
|
|
89
|
+
<Input defaultValue={record?.name} />
|
|
90
|
+
</Modal>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const CreateDrawer = ({ record, visible, onClose }) => (
|
|
94
|
+
<Drawer open={visible} title="新建" onClose={() => onClose()}>
|
|
95
|
+
<Input placeholder="姓名" />
|
|
96
|
+
<Button type="primary" onClick={() => { message.success("新建成功"); onClose(1); }}>保存</Button>
|
|
97
|
+
</Drawer>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
export default () => (
|
|
101
|
+
<ListPage
|
|
102
|
+
title="用户列表"
|
|
103
|
+
initialValues={{ filterValues: {}, pageSize: 10, currentPage: 1 }}
|
|
104
|
+
extra={(ctx) => <Button type="primary" onClick={() => ctx.showFloat("create", {}, true)}>新建</Button>}
|
|
105
|
+
filter={{
|
|
106
|
+
items: [
|
|
107
|
+
{ name: "name", label: "姓名", component: <Input allowClear placeholder="姓名" /> },
|
|
108
|
+
{ name: "dept", label: "部门", component: <Select allowClear options={[{ label: "研发", value: "研发" }]} /> },
|
|
109
|
+
{ name: "status", label: "状态", component: <Select allowClear options={[{ label: "启用", value: "启用" }]} /> },
|
|
110
|
+
],
|
|
111
|
+
}}
|
|
112
|
+
toolbar={{ render: ({ ctx }) => <Button onClick={() => ctx.refreshTable()}>刷新</Button> }}
|
|
113
|
+
table={{
|
|
114
|
+
title: "数据列表",
|
|
115
|
+
rowSelection: true,
|
|
116
|
+
rowTitleKey: "name",
|
|
117
|
+
extra: (ctx) => <Button size="small" onClick={() => ctx.refreshTable()}>刷新</Button>,
|
|
118
|
+
rowKey: "id",
|
|
119
|
+
columns: [
|
|
120
|
+
{ title: "ID", dataIndex: "id", key: "id", width: 100, renderer: "copyable" },
|
|
121
|
+
{ title: "姓名", dataIndex: "name", key: "name", width: 100, renderer: "link", rendererOptions: { onClick: (record, ctx) => ctx?.showFloat("detail", record) } },
|
|
122
|
+
{ title: "部门", dataIndex: "dept", key: "dept", width: 90, renderer: "tag", rendererOptions: { colorMap: { 研发: "blue" } } },
|
|
123
|
+
{ title: "状态", dataIndex: "status", key: "status", width: 90, renderer: "tag", rendererOptions: { colorMap: { 启用: "success" } } },
|
|
124
|
+
{ title: "价格", dataIndex: "price", key: "price", width: 110, renderer: "money", rendererOptions: { precision: 2, prefix: "¥" } },
|
|
125
|
+
{ title: "创建时间", dataIndex: "createTime", key: "createTime", width: 110, renderer: "date", rendererOptions: { format: "YYYY-MM-DD" } },
|
|
126
|
+
{
|
|
127
|
+
title: "操作",
|
|
128
|
+
key: "action",
|
|
129
|
+
width: 140,
|
|
130
|
+
fixed: "right",
|
|
131
|
+
component: ({ ctx, record }) => (
|
|
132
|
+
<Space size="small">
|
|
133
|
+
<Button type="link" size="small" onClick={() => ctx.showFloat("detail", record)}>详情</Button>
|
|
134
|
+
<Button type="link" size="small" onClick={() => ctx.showFloat("edit", record, true)}>编辑</Button>
|
|
135
|
+
</Space>
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
}}
|
|
140
|
+
request={request}
|
|
141
|
+
floats={[
|
|
142
|
+
{ key: "detail", render: DetailModal },
|
|
143
|
+
{ key: "edit", render: EditModal },
|
|
144
|
+
{ key: "create", render: CreateDrawer },
|
|
145
|
+
]}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 3. 带初始状态(initialValues)
|
|
151
|
+
|
|
152
|
+
`initialValues` 用于预填筛选条件、分页等,组件首次挂载时生效。
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
<ListPage
|
|
156
|
+
title="用户列表"
|
|
157
|
+
initialValues={{
|
|
158
|
+
filterValues: { name: "张三", status: "启用" },
|
|
159
|
+
pageSize: 20,
|
|
160
|
+
currentPage: 1,
|
|
161
|
+
}}
|
|
162
|
+
filter={{ items: filterItems }}
|
|
163
|
+
table={{ columns, rowKey: "id" }}
|
|
164
|
+
request={request}
|
|
165
|
+
/>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## 4. 带行选择
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
table={{
|
|
172
|
+
columns,
|
|
173
|
+
rowKey: "id",
|
|
174
|
+
rowSelection: true,
|
|
175
|
+
rowTitleKey: "name",
|
|
176
|
+
}}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## 5. 内置渲染器速查(listpage-components)
|
|
180
|
+
|
|
181
|
+
| renderer | 说明 | rendererOptions 示例 |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| text | 文本 | - |
|
|
184
|
+
| ellipsis | 省略 | `{ maxLength: 10 }` |
|
|
185
|
+
| tag | 标签 | `{ colorMap: { 启用: "success" } }` |
|
|
186
|
+
| badge | 徽标 | `{ colorMap: { 0: "default" } }` |
|
|
187
|
+
| date | 日期 | `{ format: "YYYY-MM-DD" }` |
|
|
188
|
+
| datetime | 日期时间 | `{ format: "YYYY-MM-DD HH:mm:ss" }` |
|
|
189
|
+
| number | 数字 | `{ precision: 2 }` |
|
|
190
|
+
| money | 金额 | `{ precision: 2, prefix: "¥" }` |
|
|
191
|
+
| percent | 百分比 | `{ precision: 1 }` |
|
|
192
|
+
| boolean | 布尔 | `{ labels: ["否", "是"] }` |
|
|
193
|
+
| link | 链接 | `{ onClick: (record, ctx) => ctx?.showFloat("detail", record) }` |
|
|
194
|
+
| enum | 枚举 | `{ options: [{ value: 0, label: "男" }] }` |
|
|
195
|
+
| copyable | 可复制 | - |
|
|
196
|
+
| avatar | 头像 | `{ fallback: "?" }` |
|
|
197
|
+
| image | 图片 | `{ width: 40, height: 40 }` |
|
|
198
|
+
|
|
199
|
+
## 6. 复杂结构(config 拆分目录)
|
|
200
|
+
|
|
201
|
+
目录结构:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
pages/MemberList/
|
|
205
|
+
├── index.tsx
|
|
206
|
+
├── config/
|
|
207
|
+
│ ├── index.tsx
|
|
208
|
+
│ ├── columns.tsx
|
|
209
|
+
│ ├── filters.tsx
|
|
210
|
+
│ ├── floats.tsx
|
|
211
|
+
│ └── request.ts
|
|
212
|
+
└── components/
|
|
213
|
+
└── SalesFormFloat.tsx
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**config/index.tsx** 汇总导出:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
export { columns } from "./columns";
|
|
220
|
+
export { filters } from "./filters";
|
|
221
|
+
export { floats } from "./floats";
|
|
222
|
+
export { request } from "./request";
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**index.tsx** 组装(listpage-components):
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import { ListPage } from "listpage-components";
|
|
229
|
+
import { columns, filters, floats, request } from "./config";
|
|
230
|
+
|
|
231
|
+
export default function MemberListPage() {
|
|
232
|
+
return (
|
|
233
|
+
<ListPage
|
|
234
|
+
title="会员列表"
|
|
235
|
+
filter={{ items: filters }}
|
|
236
|
+
table={{ columns, rowKey: "id" }}
|
|
237
|
+
request={request}
|
|
238
|
+
floats={floats}
|
|
239
|
+
initialValues={{ pageSize: 10, currentPage: 1 }}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
@@ -16,14 +16,16 @@
|
|
|
16
16
|
"@nestjs/common": "^11.0.1",
|
|
17
17
|
"@nestjs/core": "^11.0.1",
|
|
18
18
|
"@nestjs/platform-express": "^11.0.1",
|
|
19
|
-
|
|
20
|
-
"prisma": "^
|
|
19
|
+
"prisma": "^7.0.0",
|
|
20
|
+
"@prisma/adapter-mariadb": "^7.0.0",
|
|
21
|
+
"@prisma/client": "^7.0.0",
|
|
22
|
+
"@prisma/internals": "^7.0.0",
|
|
21
23
|
"@nestjs/config": "^4.0.0",
|
|
22
24
|
"reflect-metadata": "^0.2.2",
|
|
23
25
|
"class-transformer": "^0.5.1",
|
|
24
26
|
"class-validator": "~0.14.2",
|
|
25
27
|
"rxjs": "^7.8.1",
|
|
26
|
-
"listpage-next-nest": "~0.0.
|
|
28
|
+
"listpage-next-nest": "~0.0.292"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
29
31
|
"@nestjs/schematics": "^11.0.0",
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Module } from '@nestjs/common';
|
|
2
2
|
import { PrismaModule } from './prisma/prisma.module';
|
|
3
|
+
import { ConfigModule } from '@nestjs/config';
|
|
3
4
|
|
|
4
5
|
@Module({
|
|
5
|
-
imports: [
|
|
6
|
+
imports: [
|
|
7
|
+
ConfigModule.forRoot({
|
|
8
|
+
isGlobal: true,
|
|
9
|
+
}),
|
|
10
|
+
PrismaModule,
|
|
11
|
+
],
|
|
6
12
|
})
|
|
7
13
|
export class AppModule {}
|
|
@@ -1,8 +1,25 @@
|
|
|
1
|
-
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
1
|
+
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
2
3
|
import { PrismaClient } from '@prisma/client';
|
|
4
|
+
import { PrismaMariaDb } from '@prisma/adapter-mariadb';
|
|
3
5
|
|
|
4
6
|
@Injectable()
|
|
5
|
-
export class PrismaService
|
|
7
|
+
export class PrismaService
|
|
8
|
+
extends PrismaClient
|
|
9
|
+
implements OnModuleInit, OnModuleDestroy
|
|
10
|
+
{
|
|
11
|
+
constructor(configService: ConfigService) {
|
|
12
|
+
const adapter = new PrismaMariaDb({
|
|
13
|
+
host: configService.get<string>('DB_HOST'),
|
|
14
|
+
port: Number(configService.get<number>('DB_PORT')),
|
|
15
|
+
user: configService.get<string>('DB_USER'),
|
|
16
|
+
password: configService.get<string>('DB_PASSWORD'),
|
|
17
|
+
database: configService.get<string>('DB_NAME'),
|
|
18
|
+
connectionLimit: Number(configService.get<number>('DB_CONNECTION_LIMIT')),
|
|
19
|
+
});
|
|
20
|
+
super({ adapter });
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
async onModuleInit() {
|
|
7
24
|
await this.$connect();
|
|
8
25
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"react": "^19.2.0",
|
|
14
14
|
"react-dom": "^19.2.0",
|
|
15
|
-
"listpage-next": "~0.0.
|
|
15
|
+
"listpage-next": "~0.0.292",
|
|
16
16
|
"react-router-dom": ">=6.0.0",
|
|
17
17
|
"@ant-design/v5-patch-for-react-19": "~1.0.3",
|
|
18
18
|
"ahooks": "^3.9.5",
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"styled-components": "^6.1.19",
|
|
24
24
|
"mobx": "~6.15.0",
|
|
25
25
|
"@ant-design/icons": "~6.0.2",
|
|
26
|
+
"listpage-components": "~0.0.292",
|
|
27
|
+
"lucide-react": "~0.575.0"
|
|
26
28
|
"mobx-react-lite": "~4.1.1"
|
|
27
29
|
},
|
|
28
30
|
"devDependencies": {
|
|
@@ -1,17 +1,68 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
|
3
|
+
import { ConfigProvider } from 'antd';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import 'dayjs/locale/zh-cn';
|
|
2
6
|
import { menus } from './router/menus';
|
|
3
|
-
|
|
7
|
+
import { MainLayout } from './layouts/MainLayout';
|
|
4
8
|
import './App.css';
|
|
5
9
|
|
|
10
|
+
import 'listpage-components/ui.css';
|
|
11
|
+
|
|
12
|
+
dayjs.locale('zh-cn');
|
|
13
|
+
|
|
14
|
+
const RequireAuth = ({ children }: { children: React.ReactElement }) => {
|
|
15
|
+
const token = localStorage.getItem('token');
|
|
16
|
+
if (!token) {
|
|
17
|
+
return <Navigate to="/login" replace />;
|
|
18
|
+
}
|
|
19
|
+
return children;
|
|
20
|
+
};
|
|
21
|
+
|
|
6
22
|
export default () => {
|
|
7
23
|
return (
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
24
|
+
<ConfigProvider
|
|
25
|
+
theme={{
|
|
26
|
+
token: {
|
|
27
|
+
colorPrimary: '#00A67E', // Medical Green
|
|
28
|
+
borderRadius: 4,
|
|
29
|
+
controlHeight: 28,
|
|
30
|
+
},
|
|
31
|
+
components: {
|
|
32
|
+
Switch: {
|
|
33
|
+
trackHeightSM: 16,
|
|
34
|
+
handleSizeSM: 12,
|
|
35
|
+
trackPadding: 2,
|
|
36
|
+
trackMinWidthSM: 28,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<BrowserRouter basename="/__APP_NAME__">
|
|
42
|
+
<Routes>
|
|
43
|
+
<Route
|
|
44
|
+
path="/"
|
|
45
|
+
element={
|
|
46
|
+
<RequireAuth>
|
|
47
|
+
<MainLayout />
|
|
48
|
+
</RequireAuth>
|
|
49
|
+
}
|
|
50
|
+
>
|
|
51
|
+
<Route
|
|
52
|
+
index
|
|
53
|
+
element={<Navigate to={`/${menus[0].key}`} replace />}
|
|
54
|
+
/>
|
|
55
|
+
{menus.map((menu) => (
|
|
56
|
+
<Route
|
|
57
|
+
key={menu.key}
|
|
58
|
+
path={`${menu.key}/*`}
|
|
59
|
+
element={menu.element}
|
|
60
|
+
/>
|
|
61
|
+
))}
|
|
62
|
+
</Route>
|
|
63
|
+
</Routes>
|
|
64
|
+
</BrowserRouter>
|
|
65
|
+
</ConfigProvider>
|
|
16
66
|
);
|
|
17
67
|
};
|
|
68
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useLocation, useNavigate, Outlet, matchPath } from 'react-router-dom';
|
|
3
|
+
import { Activity } from 'lucide-react';
|
|
4
|
+
import { RouterAdminLayout } from 'listpage-components';
|
|
5
|
+
|
|
6
|
+
import { menus } from '../router/menus';
|
|
7
|
+
|
|
8
|
+
export const MainLayout: React.FC = () => {
|
|
9
|
+
return (
|
|
10
|
+
<div className="h-screen overflow-hidden">
|
|
11
|
+
<RouterAdminLayout
|
|
12
|
+
title="你的应用标题"
|
|
13
|
+
logo={<Activity />}
|
|
14
|
+
collapsedWidth={70}
|
|
15
|
+
menus={menus}
|
|
16
|
+
routerAdapter={{
|
|
17
|
+
useLocation,
|
|
18
|
+
useNavigate,
|
|
19
|
+
matchPath: (pattern, pathname) => {
|
|
20
|
+
const result = matchPath({ path: pattern, end: false }, pathname);
|
|
21
|
+
if (!result) return null;
|
|
22
|
+
return {
|
|
23
|
+
params: result.params as any,
|
|
24
|
+
pathname: result.pathname,
|
|
25
|
+
pattern,
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<Outlet />
|
|
31
|
+
</RouterAdminLayout>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const menus: MenuItem[] = [
|
|
1
|
+
export const menus: any[] = [
|
|
4
2
|
{
|
|
5
3
|
key: 'menu1',
|
|
4
|
+
path: 'menu1',
|
|
6
5
|
label: '菜单一',
|
|
7
|
-
|
|
6
|
+
icon: <span>📈</span>,
|
|
7
|
+
element: <div>菜单1</div>,
|
|
8
8
|
},
|
|
9
|
-
|
|
10
|
-
key: 'menu2',
|
|
11
|
-
label: '菜单二',
|
|
12
|
-
element: <div>菜单二的内容</div>
|
|
13
|
-
},
|
|
14
|
-
];
|
|
9
|
+
];
|
package/templates/rush-template/docs/ListPage-AI/347/224/237/346/210/220/350/247/204/350/214/203.md
CHANGED
|
@@ -174,6 +174,8 @@ import {
|
|
|
174
174
|
type ListPageTableColumn,
|
|
175
175
|
type PaginationData,
|
|
176
176
|
type ListPageContext,
|
|
177
|
+
type ListPageFloatProps,
|
|
178
|
+
type FloatComponentProps,
|
|
177
179
|
} from "listpage-next";
|
|
178
180
|
import { Button, Input, Select, Space } from "antd";
|
|
179
181
|
|
|
@@ -204,6 +206,45 @@ const filters: FilterFormOption[] = [
|
|
|
204
206
|
const columns: ListPageTableColumn<User>[] = [
|
|
205
207
|
{ title: "姓名", dataIndex: "name", key: "name", component: "text" },
|
|
206
208
|
{ title: "地址", dataIndex: "address", key: "address", ellipsis: true },
|
|
209
|
+
{
|
|
210
|
+
title: '操作',
|
|
211
|
+
key: 'actions',
|
|
212
|
+
width: 160,
|
|
213
|
+
fixed: 'right',
|
|
214
|
+
component: ({ ctx, record }) => (
|
|
215
|
+
<Space>
|
|
216
|
+
<Button type="link" size="small" onClick={() => ctx.showFloat('edit', record, true)}>
|
|
217
|
+
编辑
|
|
218
|
+
</Button>
|
|
219
|
+
<Popconfirm
|
|
220
|
+
title="确定要删除吗?"
|
|
221
|
+
onConfirm={async () => {
|
|
222
|
+
await api.sales.remove(record.id);
|
|
223
|
+
message.success('删除成功');
|
|
224
|
+
ctx.refreshTable();
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
<Button type="link" size="small" danger>
|
|
228
|
+
删除
|
|
229
|
+
</Button>
|
|
230
|
+
</Popconfirm>
|
|
231
|
+
</Space>
|
|
232
|
+
),
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
const EditorModal = (props: FloatComponentProps<User>) => {
|
|
237
|
+
return <Modal></Modal>
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const EditorDrawer = (props: FloatComponentProps<User>) => {
|
|
241
|
+
return <Drawer></Drawer>
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
const floats: ListPageFloatProps[] = [
|
|
246
|
+
{ key: 'edit', render: EditorModal },
|
|
247
|
+
{ key: 'create', render: EditorDrawer },
|
|
207
248
|
];
|
|
208
249
|
|
|
209
250
|
const request = async (
|