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 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.286",
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
+ ```
@@ -0,0 +1,7 @@
1
+ DATABASE_URL=""
2
+ DB_HOST=""
3
+ DB_PORT=3306
4
+ DB_USER="root"
5
+ DB_PASSWORD=""
6
+ DB_NAME=""
7
+ DB_CONNECTION_LIMIT=20
@@ -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
- "@prisma/client": "^6.0.0",
20
- "prisma": "^6.0.0",
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.286"
28
+ "listpage-next-nest": "~0.0.292"
27
29
  },
28
30
  "devDependencies": {
29
31
  "@nestjs/schematics": "^11.0.0",
@@ -3,8 +3,7 @@ generator client {
3
3
  }
4
4
 
5
5
  datasource db {
6
- provider = "sqlite"
7
- url = "file:./dev.db"
6
+ provider = "mysql"
8
7
  }
9
8
 
10
9
  model User {
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'prisma/config';
2
+ import 'dotenv/config';
3
+
4
+ export default defineConfig({
5
+ schema: 'prisma/schema.prisma',
6
+ datasource: {
7
+ url: process.env.DATABASE_URL!,
8
+ },
9
+ });
@@ -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: [PrismaModule],
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 extends PrismaClient implements OnModuleInit {
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.286",
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 { App } from 'listpage-next/features/ReactApp';
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
- <App
9
- basename="/__APP_NAME__"
10
- icon={<span>Logo</span>}
11
- title="Next Page"
12
- menus={menus}
13
- hasAuth={() => true}
14
- loginElement={<div>登录页</div>}
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
- import type { MenuItem } from 'listpage-next';
2
-
3
- export const menus: MenuItem[] = [
1
+ export const menus: any[] = [
4
2
  {
5
3
  key: 'menu1',
4
+ path: 'menu1',
6
5
  label: '菜单一',
7
- element: <div>菜单一的内容</div>
6
+ icon: <span>📈</span>,
7
+ element: <div>菜单1</div>,
8
8
  },
9
- {
10
- key: 'menu2',
11
- label: '菜单二',
12
- element: <div>菜单二的内容</div>
13
- },
14
- ];
9
+ ];
@@ -9,7 +9,7 @@
9
9
  "publish": "ts-node src/publish.ts"
10
10
  },
11
11
  "dependencies": {
12
- "listpage-next-deploy": "0.0.286"
12
+ "listpage-next-deploy": "0.0.292"
13
13
  },
14
14
  "devDependencies": {
15
15
  "@types/node": "^20.0.0",
@@ -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 (