listpage_cli 0.0.286 → 0.0.291
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 +19 -0
- package/bin/copy.js +4 -0
- package/bin/prompts.js +2 -0
- package/package.json +3 -2
- package/skills/listpage/SKILL.md +19 -0
- package/skills/listpage/api.md +92 -0
- package/skills/listpage/examples.md +149 -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,8 @@ 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");
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
7
9
|
const path_1 = __importDefault(require("path"));
|
|
8
10
|
const prompts_1 = require("./prompts");
|
|
9
11
|
const copy_1 = require("./copy");
|
|
@@ -52,6 +54,8 @@ async function main() {
|
|
|
52
54
|
return initCmd();
|
|
53
55
|
if (cmd === "sync-docs")
|
|
54
56
|
return syncDocsCmd();
|
|
57
|
+
if (cmd === "install-skill")
|
|
58
|
+
return installSkillCmd();
|
|
55
59
|
(0, prompts_1.printHelp)();
|
|
56
60
|
}
|
|
57
61
|
main();
|
|
@@ -65,3 +69,18 @@ async function syncDocsCmd() {
|
|
|
65
69
|
(0, copy_1.syncDirWithRename)(sourceDocs, destDocs);
|
|
66
70
|
console.log("已同步 .trae 和 docs 到执行目录的同级目录");
|
|
67
71
|
}
|
|
72
|
+
async function installSkillCmd() {
|
|
73
|
+
const args = process.argv.slice(2);
|
|
74
|
+
const projectMode = args.includes("--project");
|
|
75
|
+
const skillName = args.find((a) => a !== "install-skill" && a !== "--project") || "listpage";
|
|
76
|
+
const sourceDir = path_1.default.join(__dirname, "..", "skills", skillName);
|
|
77
|
+
if (!(0, fs_1.existsSync)(sourceDir)) {
|
|
78
|
+
console.error(`技能 ${skillName} 不存在`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const targetDir = projectMode
|
|
82
|
+
? path_1.default.join(process.cwd(), ".cursor", "skills", skillName)
|
|
83
|
+
: path_1.default.join(os_1.default.homedir(), ".cursor", "skills", skillName);
|
|
84
|
+
(0, copy_1.copySkillDir)(sourceDir, targetDir);
|
|
85
|
+
console.log(`已安装 ${skillName} 技能到 ${targetDir}`);
|
|
86
|
+
}
|
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.291",
|
|
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,19 @@
|
|
|
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
|
+
使用 `listpage-next`。示例与 API 见 [examples.md](examples.md)、[api.md](api.md)。
|
|
9
|
+
|
|
10
|
+
## 核心要点
|
|
11
|
+
|
|
12
|
+
- **request**:`(pageParams, filterValues) => Promise<{ list, total, current, pageSize }>`
|
|
13
|
+
- **必配**:`request`、`table.columns`、`table.tableProps.rowKey`
|
|
14
|
+
- **浮窗**:`floats=[{ key, render }]`,`ctx.showFloat(key, record, true)` 关闭后刷新
|
|
15
|
+
|
|
16
|
+
## 目录结构
|
|
17
|
+
|
|
18
|
+
- **简单页面**:单文件内联 columns、filters、request
|
|
19
|
+
- **复杂页面**(筛选项 > 3、列 > 5、浮窗 > 1):`pages/<Feature>/config/{columns,filters,floats,request}.tsx` + `components/`
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# API
|
|
2
|
+
|
|
3
|
+
## ListPageProps
|
|
4
|
+
|
|
5
|
+
| 属性 | 说明 | 类型 | 默认值 | 必填 |
|
|
6
|
+
| --- | --- | --- | --- | --- |
|
|
7
|
+
| `styles` | 页面容器样式覆盖 | `{ page?: React.CSSProperties }` | - | 否 |
|
|
8
|
+
| `storageKey` | 本地存储键名(分页/筛选持久化) | `string` | - | 否 |
|
|
9
|
+
| `initialValues` | 初始值(在无 `state` 与缓存时生效) | `{ filterValues?: any; pageSize?: number; currentPage?: number }` | - | 否 |
|
|
10
|
+
| `state` | 外部初始化值(优先级最高,非受控) | `{ filterValues?: any; pageSize?: number; currentPage?: number }` | - | 否 |
|
|
11
|
+
| `request` | 数据请求函数,返回分页数据 | `(pageParams: { current?: number; pageSize?: number; sort?: string }, filterValues: any) => Promise<{ total: number; pageSize: number; current: number; list: any[] }>` | - | 是 |
|
|
12
|
+
| `header` | 页面标题与右侧操作 | `{ title?: React.ReactNode; extra?: React.ReactNode | ((ctx) => React.ReactNode) }` | - | 否 |
|
|
13
|
+
| `filter` | 筛选配置 | `{ labelInline?: boolean; options: FilterFormOption[]; onReset?: () => void; onChange?: (value) => void }` | - | 否 |
|
|
14
|
+
| `toolbar` | 页面级工具栏 | `{ render: (ctx) => React.ReactNode }` | - | 否 |
|
|
15
|
+
| `table` | 表格配置 | `DataTableProps` | - | 是 |
|
|
16
|
+
| `floats` | 浮窗定义 | `Array<{ key: string; render: (props: FloatComponentProps) => JSX.Element }>` | - | 否 |
|
|
17
|
+
|
|
18
|
+
## FilterFormOption
|
|
19
|
+
|
|
20
|
+
| 属性 | 说明 | 类型 | 默认值 | 必填 |
|
|
21
|
+
| --- | --- | --- | --- | --- |
|
|
22
|
+
| `name` | 字段名 | `string` | - | 是 |
|
|
23
|
+
| `label` | 标签 | `React.ReactNode` | - | 否 |
|
|
24
|
+
| `component` | 组件类型或自定义节点 | `'input' | 'select' | 'async-select' | 'daterange' | ReactElement` | `'input'` | 否 |
|
|
25
|
+
| `colSpan` | 栅格占比(6 表示半行) | `number` | `2` | 否 |
|
|
26
|
+
| `props` | 组件 props(随类型变化) | `ComponentProps<Type>` | - | 否 |
|
|
27
|
+
| `formItemProps` | antd `Form.Item` 属性(去除 children) | `Omit<FormItemProps, 'children'>` | - | 否 |
|
|
28
|
+
|
|
29
|
+
组件映射与值约定:
|
|
30
|
+
- `'input'` → antd `Input`
|
|
31
|
+
- `'select'` → antd `Select`
|
|
32
|
+
- `'async-select'` → 组件库 `AsyncSelect`,`request` 返回 `{ list, total, current, pageSize }`
|
|
33
|
+
- `'daterange'` → 增强版 `RangePicker`,提交值为 `[start: string, end: string]`
|
|
34
|
+
|
|
35
|
+
## DataTableProps(`ListPage.table`)
|
|
36
|
+
|
|
37
|
+
| 属性 | 说明 | 类型 | 默认值 | 必填 |
|
|
38
|
+
| --- | --- | --- | --- | --- |
|
|
39
|
+
| `columns` | 列配置 | `ListPageTableColumn[]` | `[]` | 否 |
|
|
40
|
+
| `title` | 表格卡片标题 | `React.ReactNode` | - | 否 |
|
|
41
|
+
| `extra` | 标题右侧操作(函数可访问 ctx) | `React.ReactNode | ((ctx) => React.ReactNode)` | - | 否 |
|
|
42
|
+
| `tableProps` | antd `Table` 其他属性(除 `dataSource/title/columns`) | `Omit<TableProps, 'dataSource' | 'title' | 'columns'>` | - | 否 |
|
|
43
|
+
| `rowSelectionType` | 选择类型 | `'checkbox' | 'radio'` | - | 否 |
|
|
44
|
+
| `rowTitleKey` | 标题展示所选关键字段 | `string` | - | 否 |
|
|
45
|
+
|
|
46
|
+
分页说明:底部中文本地化;`pageSizeOptions=[10,20,50,100]`;`showTotal="start-end 共 total 条"`;`tableProps.pagination===false` 隐藏分页。
|
|
47
|
+
|
|
48
|
+
## ListPageTableColumn
|
|
49
|
+
|
|
50
|
+
| 属性 | 说明 | 类型 | 默认值 | 必填 |
|
|
51
|
+
| --- | --- | --- | --- | --- |
|
|
52
|
+
| `title` | 列头 | `React.ReactNode` | - | 否 |
|
|
53
|
+
| `dataIndex` | 数据字段 | `string` | - | 否 |
|
|
54
|
+
| `key` | 唯一键 | `string` | - | 否 |
|
|
55
|
+
| `width` | 列宽 | `number` | - | 否 |
|
|
56
|
+
| `fixed` | 固定列 | `'left' | 'right'` | - | 否 |
|
|
57
|
+
| `ellipsis` | 省略展示 | `boolean` | - | 否 |
|
|
58
|
+
| `component` | 内置或自定义渲染 | `'text' | 'time' | 'tag' | 'link' | 'switch' | ((value, record, index, ctx) => React.ReactNode)` | - | 否 |
|
|
59
|
+
| `props` | 随 `component` 类型变化的 props | `any` | - | 否 |
|
|
60
|
+
|
|
61
|
+
内置渲染 props 说明:
|
|
62
|
+
- `text`:透传 `Typography.Text`,默认 `ellipsis={{ tooltip: true }}`
|
|
63
|
+
- `time`:`{ format?: string }`,无值显示 `'-'`
|
|
64
|
+
- `tag`:透传 `TagProps`
|
|
65
|
+
- `link`:`{ titlePropName?: string | (record)=>string; hrefPropName?: string | (record)=>string; target?: HTMLAttributeAnchorTarget }`
|
|
66
|
+
- `switch`:`{ syncChange?: (value: boolean, record) => Promise<void> }`
|
|
67
|
+
|
|
68
|
+
## Floats(浮窗)
|
|
69
|
+
|
|
70
|
+
| 属性 | 说明 | 类型 |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `key` | 浮窗标识,短横线小写 | `string` |
|
|
73
|
+
| `render` | 浮窗组件渲染函数 | `(props: { record: any; visible: boolean; onClose: () => void }) => JSX.Element` |
|
|
74
|
+
|
|
75
|
+
调用方式:在列或工具栏中使用 `ctx.showFloat(key, record, onCloseCallback)`;传 `true` 关闭后自动刷新列表,传函数自定义回调。
|
|
76
|
+
|
|
77
|
+
## ListPageStore(上下文)
|
|
78
|
+
|
|
79
|
+
| 名称 | 说明 | 类型/签名 |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `filters` | 当前筛选条件 | `any` |
|
|
82
|
+
| `dataSource` | 表格数据源 | `any[]` |
|
|
83
|
+
| `pagination` | 分页信息 | `{ current: number; pageSize: number; total: number }` |
|
|
84
|
+
| `loadingData` | 加载状态 | `boolean` |
|
|
85
|
+
| `selection` | 选择集 | `{ selectedRowKeys: Key[]; selectedRows: any[] }` |
|
|
86
|
+
| `submitFilters` | 提交筛选并重置页码 | `(value) => void` |
|
|
87
|
+
| `showFloat` | 打开浮窗 | `(key: string, record: any, onCloseCallback?: true | (() => void)) => void` |
|
|
88
|
+
| `hideFloat` | 关闭浮窗 | `() => void` |
|
|
89
|
+
| `updatePage` | 更新页码与页大小 | `(page: number, pageSize: number) => void` |
|
|
90
|
+
| `fetchTableData` | 拉取数据 | `() => Promise<void>` |
|
|
91
|
+
| `refreshTable` | 刷新数据 | `() => void` |
|
|
92
|
+
| `listen/emit` | 事件机制 | `(name: string, cb: (data:any)=>void) / (name: string, data:any) => void` |
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# ListPage 示例
|
|
2
|
+
|
|
3
|
+
## 1. 最小示例(单文件内联)
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { ListPage, type FilterFormOption, type ListPageTableColumn } from 'listpage-next';
|
|
7
|
+
import { Button, Input, Select, Space } from 'antd';
|
|
8
|
+
|
|
9
|
+
const filters: FilterFormOption[] = [
|
|
10
|
+
{ name: 'name', label: '姓名', component: <Input placeholder="请输入" /> },
|
|
11
|
+
{ name: 'status', label: '状态', component: <Select allowClear options={[{ value: 'enabled', label: '启用' }, { value: 'disabled', label: '停用' }]} /> },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const columns: ListPageTableColumn[] = [
|
|
15
|
+
{ title: '姓名', dataIndex: 'name', key: 'name', component: 'text' },
|
|
16
|
+
{ title: '地址', dataIndex: 'address', key: 'address', ellipsis: true },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const request = async ({ current = 1, pageSize = 10 }, filterValues: any) => {
|
|
20
|
+
const all = Array.from({ length: 100 }).map((_, i) => ({ id: i + 1, name: `Edward ${i + 1}`, address: `Address ${i + 1}` }));
|
|
21
|
+
let list = all.filter((r) => !filterValues?.name || r.name.includes(filterValues.name));
|
|
22
|
+
const skip = (current - 1) * pageSize;
|
|
23
|
+
return { list: list.slice(skip, skip + pageSize), total: list.length, current, pageSize };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default function Page() {
|
|
27
|
+
return (
|
|
28
|
+
<ListPage
|
|
29
|
+
storageKey="demo-listpage"
|
|
30
|
+
initialValues={{ currentPage: 1, pageSize: 10 }}
|
|
31
|
+
request={request}
|
|
32
|
+
header={{ title: '用户列表', extra: (ctx) => <Space><Button onClick={() => ctx.refreshTable()}>刷新</Button></Space> }}
|
|
33
|
+
filter={{ options: filters }}
|
|
34
|
+
table={{ columns, tableProps: { rowKey: 'id' } }}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 2. 带浮窗(新增 Drawer)
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { ListPage, type FilterFormOption, type ListPageTableColumn } from 'listpage-next';
|
|
44
|
+
import { Button, Input, Drawer, Space } from 'antd';
|
|
45
|
+
import { useState } from 'react';
|
|
46
|
+
|
|
47
|
+
const filters: FilterFormOption[] = [{ name: 'name', label: '姓名', component: <Input placeholder="请输入" /> }];
|
|
48
|
+
const columns: ListPageTableColumn[] = [
|
|
49
|
+
{ title: '姓名', dataIndex: 'name', key: 'name', component: 'text' },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const request = async ({ current = 1, pageSize = 10 }, f: any) => {
|
|
53
|
+
const list = [{ id: 1, name: 'Test' }];
|
|
54
|
+
return { list, total: 1, current, pageSize };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const CreateDrawer = (props: { record: any; visible: boolean; onClose: () => void }) => {
|
|
58
|
+
const { visible, onClose } = props;
|
|
59
|
+
return (
|
|
60
|
+
<Drawer open={visible} title="新增" onClose={onClose}>
|
|
61
|
+
<Input placeholder="姓名" />
|
|
62
|
+
<Button type="primary" onClick={onClose}>保存</Button>
|
|
63
|
+
</Drawer>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default function Page() {
|
|
68
|
+
return (
|
|
69
|
+
<ListPage
|
|
70
|
+
storageKey="LISTPAGE_USER"
|
|
71
|
+
request={request}
|
|
72
|
+
header={{ title: '用户', extra: (ctx) => <Button type="primary" onClick={() => ctx.showFloat('create', {}, true)}>新增</Button> }}
|
|
73
|
+
filter={{ options: filters }}
|
|
74
|
+
table={{ columns, tableProps: { rowKey: 'id' } }}
|
|
75
|
+
floats={[{ key: 'create', render: CreateDrawer }]}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 3. 带行选择
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
table={{
|
|
85
|
+
columns,
|
|
86
|
+
rowSelectionType: 'checkbox',
|
|
87
|
+
rowTitleKey: 'name',
|
|
88
|
+
tableProps: { rowKey: 'id' },
|
|
89
|
+
}}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 4. 带 toolbar
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
toolbar={{
|
|
96
|
+
render: (ctx) => (
|
|
97
|
+
<Space>
|
|
98
|
+
<Button onClick={() => ctx.refreshTable()}>刷新</Button>
|
|
99
|
+
<Button>导出</Button>
|
|
100
|
+
</Space>
|
|
101
|
+
),
|
|
102
|
+
}}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 5. 复杂结构(config 拆分目录)
|
|
106
|
+
|
|
107
|
+
目录结构:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
pages/MemberList/
|
|
111
|
+
├── index.tsx
|
|
112
|
+
├── config/
|
|
113
|
+
│ ├── index.tsx
|
|
114
|
+
│ ├── columns.tsx
|
|
115
|
+
│ ├── filters.tsx
|
|
116
|
+
│ ├── floats.tsx
|
|
117
|
+
│ └── request.ts
|
|
118
|
+
└── components/
|
|
119
|
+
└── SalesFormFloat.tsx
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**config/index.tsx** 汇总导出:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
export { columns } from './columns';
|
|
126
|
+
export { filters } from './filters';
|
|
127
|
+
export { floats } from './floats';
|
|
128
|
+
export { request } from './request';
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**index.tsx** 组装:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import { ListPage } from 'listpage-next';
|
|
135
|
+
import { columns, filters, floats, request } from './config';
|
|
136
|
+
|
|
137
|
+
export default function MemberListPage() {
|
|
138
|
+
return (
|
|
139
|
+
<ListPage
|
|
140
|
+
storageKey="LISTPAGE_MEMBER"
|
|
141
|
+
request={request}
|
|
142
|
+
header={{ title: '会员列表' }}
|
|
143
|
+
filter={{ options: filters }}
|
|
144
|
+
table={{ columns, tableProps: { rowKey: 'id' } }}
|
|
145
|
+
floats={floats}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
@@ -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.291"
|
|
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.291",
|
|
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.291",
|
|
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 (
|