├── .env ├── .gitignore ├── .npmrc ├── LICENSE ├── README.cn.md ├── README.md ├── next-env.d.ts ├── next.config.js ├── package.json ├── package ├── .npmrc ├── README.cn.md ├── README.md ├── package.json ├── postcss.config.mjs ├── public │ └── image.png ├── src │ ├── DataTable.tsx │ ├── TableBody.tsx │ ├── helper.tsx │ ├── index.tsx │ └── tailwind.css ├── tailwind.config.mjs ├── tsconfig.json └── tsup.config.ts ├── postcss.config.js ├── prisma ├── data.json ├── schema.prisma └── seed.js ├── public └── image.png ├── src ├── actions │ └── payment.ts ├── app │ ├── layout.tsx │ └── page.tsx └── lib │ └── db.ts ├── tailwind.config.js └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgres://default:uGFAIPDbzy28@ep-jolly-dawn-a4t67m9w.us-east-1.aws.neon.tech:5432/verceldb?sslmode=require" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*@nextui-org/* 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Blaine White 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.cn.md: -------------------------------------------------------------------------------- 1 | # NEXT-FAST-TABLE 2 | 3 | 🦄NEXT-FAST-TABLE 是一个原子化的,基于 Nextjs 的,开箱即用的后台管理应用前端表单组件。 4 | 5 |  6 | 7 | [Engligh](./README.md) | [中文](./README.cn.md) 8 | 9 |  10 |  11 |  12 | 13 | ## 目录 14 | 15 | 1. [介绍](#介绍) 16 | 2. [功能特点](#功能特点) 17 | 3. [在线演示](#在线演示) 18 | 4. [安装](#安装) 19 | 5. [快速开始](#快速开始) 20 | - [创建 API 程序](#创建-api-程序) 21 | - [定义列并在页面中使用](#定义列并在页面中使用) 22 | 6. [配置选项](#配置选项) 23 | - [HelperConfig](#helperconfig) 24 | - [TableConfig](#tableconfig) 25 | 7. [类型](#类型) 26 | 8. [完整案例](#完整案例) 27 | 9. [为什么选择 NEXT-FAST-TABLE](#开发动机) 28 | 10. [贡献和支持](#贡献和支持) 29 | 11. [License](#license) 30 | 31 | ## 介绍 32 | 33 | **NEXT-FAST-TABLE** 是一个强大且高效的表格组件,专为使用 Next.js 的开发者设计。它简化了复杂数据展示的过程,使您能够在一分钟内快速创建和集成表格到您的应用程序中。 34 | 35 | 作为独立开发者, 你可以像是调用库一样快速创建一个可用的后台管理框架,包含了常见的增删改查等常规行为以及过滤、导出等常见需求。通过 NEXT-FAST-TABLE, 你可以把精力更多的放在核心的业务上而不是后台管理上。这个工具能够让`独立开发者`(尤其是 Nextjs 开发者)在`数分钟内`(而不是数小时)`开发并上线`一个可用的`后台管理`MVP 36 | 37 | ## 功能特点 38 | 39 | - **🔥 易于使用**:利用 Server Action,无需定义接口,直接处理数据。当然,也可以使用 fetch 请求。 40 | - **⭐️ 预设丰富**:只需调用 Fields.string()等方法,即可生成表单 41 | - **🔧 高度可定制**:支持多种配置选项和样式自定义,满足不同应用场景的需求。 42 | - **📱 响应式设计**:自动适配各种屏幕尺寸,提供最佳用户体验。 43 | - **⚙️ 高级数据处理**:内置排序、筛选、分页、模糊搜索等功能,一键实现。 44 | - **📊 数据种类齐全**:支持多种数据类型,包括文本、数字、日期、图片等。此外,还支持 JSON 和 Array 45 | 46 | ## 在线演示 47 | 48 | 49 | DEMO 50 | 51 | 52 | ## 安装 53 | 54 | 使用你喜欢的包管理器轻松安装 NEXT-FAST-TABLE: 55 | 56 | ```bash 57 | npm install next-fast-table 58 | ``` 59 | 60 | 或 61 | 62 | ```bash 63 | yarn add next-fast-table 64 | ``` 65 | 66 | 或 67 | 68 | ```bash 69 | pnpm install next-fast-table 70 | ``` 71 | 72 | ## 快速开始 73 | 74 | 以下是一个简单的示例,展示如何在 Next.js 应用中使用 NEXT-FAST-TABLE 75 | 76 | > 注意:这只是一个最小示例,实际应用中,您可以参考本项目的完整案例。 77 | 78 | ### 创建 API 程序 79 | 80 | ```typescript 81 | "use server"; 82 | import { 83 | FetchParams, 84 | CreateParams, 85 | DeleteParams, 86 | UpdateParams, 87 | } from "next-fast-table"; 88 | 89 | // 模拟数据库 90 | let payments = [ 91 | { 92 | id: 1, 93 | username: "John Doe", 94 | email: "john@example.com", 95 | }, 96 | { 97 | id: 2, 98 | username: "Jane Smith", 99 | email: "jane@example.com", 100 | }, 101 | { 102 | id: 3, 103 | username: "Alice", 104 | email: "alice@example.com", 105 | }, 106 | ]; 107 | 108 | type Payment = { 109 | id: number; 110 | username: string; 111 | email: string; 112 | }; 113 | 114 | // 获取数据 115 | export async function onFetch(obj: FetchParams) { 116 | const pageSize = obj.pagination?.pageSize ?? 10; 117 | const pageIndex = obj.pagination?.pageIndex ?? 0; 118 | 119 | // 模拟排序 120 | const sortedPayments = payments.sort((a, b) => { 121 | if (!obj.sorting || obj.sorting.length === 0) return 0; 122 | const sort = obj.sorting[0]; 123 | const multiplier = sort.desc ? -1 : 1; 124 | if (a[sort.id] < b[sort.id]) return -1 * multiplier; 125 | if (a[sort.id] > b[sort.id]) return 1 * multiplier; 126 | return 0; 127 | }); 128 | 129 | // 模拟过滤 130 | const filteredPayments = sortedPayments.filter((payment) => { 131 | if (!obj.columnFilters || obj.columnFilters.length === 0) return true; 132 | return obj.columnFilters.every((filter) => { 133 | if ( 134 | typeof filter.value === "number" || 135 | typeof filter.value === "boolean" 136 | ) { 137 | return payment[filter.id] === filter.value; 138 | } else if (typeof filter.value === "string") { 139 | return payment[filter.id].includes(filter.value); 140 | } 141 | return false; 142 | }); 143 | }); 144 | 145 | const total = filteredPayments.length; 146 | const list = filteredPayments.slice( 147 | pageIndex * pageSize, 148 | (pageIndex + 1) * pageSize 149 | ); 150 | 151 | return { 152 | list, 153 | total, 154 | }; 155 | } 156 | 157 | // 创建数据 158 | export async function onCreate(data: CreateParams) { 159 | payments.push(data as any); 160 | } 161 | 162 | // 删除数据 163 | export async function onDelete(data: DeleteParams) { 164 | const idsToDelete = [data].flat().map((d) => d.id); 165 | payments = payments.filter((payment) => !idsToDelete.includes(payment.id)); 166 | } 167 | 168 | // 更新数据 169 | export async function onUpdate(data: UpdateParams) { 170 | payments = payments.map((payment) => 171 | payment.id === data.id ? { ...payment, ...data } : payment 172 | ); 173 | } 174 | ``` 175 | 176 | ### 定义列并在页面中使用 177 | 178 | ```typescript 179 | "use client"; 180 | import { NextFastTable, Fields } from "next-fast-table"; 181 | import { onCreate, onDelete, onFetch, onUpdate } from "YourAPIFile"; 182 | 183 | export default function DemoPage() { 184 | const field = Fields; 185 | 186 | const columns = [ 187 | field.number("id"), 188 | field.string("username"), 189 | field.email("email"), 190 | ]; 191 | 192 | return ( 193 | 200 | ); 201 | } 202 | ``` 203 | 204 | ## HelperConfig 205 | 206 | 这是一个用于控制前端表格渲染的配置选项。它提供了多种选项来控制表格的行为和数据操作。 207 | 208 | ### 配置选项 209 | 210 | - **input** 211 | - `disabled`: 在编辑模式下(包括创建和编辑)输入是否禁用。默认值为 `false`。 212 | - `required`: 在编辑模式下输入是否必填,参与表单验证。默认值为 `false`。 213 | - **list** 214 | - `hidden`: 列是否默认隐藏。如果为 `true`,则默认不显示,但可以通过列设置显示。默认值为 `false`。 215 | - **其他选项** 216 | - `label`: 列的标签或别名。默认值为 `undefined`。 217 | - `enableHiding`: 是否允许隐藏。如果为 `false`,则不显示隐藏按钮。默认值为 `true`。 218 | - `enableSorting`: 是否允许排序。如果为 `false`,则不显示排序按钮。默认值为 `true`。 219 | - `enableColumnFilter`: 列是否参与列过滤。如果为 `false`,则不显示在列过滤中。默认值为 `true`。 220 | - `enum`: 枚举值,仅在使用 `field.enum` 时有效。默认值为 `[]`。 221 | - `render`: 用于在显示状态下自定义渲染的自定义渲染函数。 222 | - 参数: 223 | - `cell`: 单元格的值。 224 | - `row`: 行数据。 225 | - 返回值: 用于渲染的 JSX 元素或字符串。 226 | 227 | ## TableConfig 228 | 229 | `TableConfig` 是 NextFastTable 组件的传参类型,其中 `columns` 和 `onFetch` 是必填的。 230 | 231 | ### 配置选项 232 | 233 | - **name** 234 | - **描述**: 表格的名称,用于生成 `tanstack-query` 的键。 235 | - **默认值**: `'next-table'` 236 | - **columns** 237 | - **描述**: 表格的列配置。 238 | - **必填**: 是 239 | - **onFetch** 240 | - **描述**: 用于获取表格数据的函数。 241 | - **参数**: 242 | - `args`: 包含分页、排序和列过滤器的对象。 243 | - **返回值**: 一个包含总项目数和数据列表(带 ID)的 Promise。 244 | - **示例**: 245 | ```javascript 246 | async function fetchData({ pagination, sorting, columnFilters }) { 247 | const data = await fetchDataFromAPI({ 248 | pagination, 249 | sorting, 250 | columnFilters, 251 | }); 252 | const total = await fetchTotalCount(); 253 | return { 254 | list: data, 255 | total, 256 | }; 257 | } 258 | ``` 259 | - **onDelete** 260 | - **描述**: 用于删除数据的函数。 261 | - **可选**: 是 262 | - **参数**: 263 | - `data`: 要删除的数据,可以是单个 ID 或 ID 数组。 264 | - **返回值**: 一个在删除完成时解析的 Promise。 265 | - **示例**: 266 | ```javascript 267 | async function deleteData(data) { 268 | await deleteDataFromAPI(data); 269 | } 270 | ``` 271 | - **onCreate** 272 | - **描述**: 用于创建新数据的函数。 273 | - **可选**: 是 274 | - **参数**: 275 | - `data`: 要创建的数据。 276 | - **返回值**: 一个在创建完成时解析的 Promise。 277 | - **示例**: 278 | ```javascript 279 | async function createData(data) { 280 | const newData = await createDataInAPI(data); 281 | return newData; 282 | } 283 | ``` 284 | - **onUpdate** 285 | - **描述**: 用于更新现有数据的函数。 286 | - **可选**: 是 287 | - **参数**: 288 | - `data`: 要更新的数据。仅发送 ID 和要更新的字段。 289 | - **返回值**: 一个在更新完成时解析的 Promise。 290 | - **示例**: 291 | ```javascript 292 | async function updateData(data) { 293 | const updatedData = await updateDataInAPI(data); 294 | return updatedData; 295 | } 296 | ``` 297 | 298 | ## 类型 299 | 300 | ```typescript 301 | type DataWithID> = { 302 | id: number | string; 303 | } & Partial; 304 | 305 | type DataOnlyId = { 306 | id: T; 307 | }; 308 | 309 | export type FetchParams = { 310 | pagination?: { pageSize: number; pageIndex: number }; 311 | sorting?: { id: string; desc: boolean }[]; 312 | columnFilters?: { id: string; value: any }[]; 313 | }; 314 | 315 | export type DeleteParams = DataOnlyId | DataOnlyId[]; 316 | 317 | export type UpdateParams> = DataWithID; 318 | 319 | export type CreateParams> = DataWithID; 320 | ``` 321 | 322 | ## 完整案例 323 | 324 | 本项目是一个最小化的 Next.js 应用,用于演示 NEXT-FAST-TABLE 的基本用法。您可以通过以下步骤在本地运行该项目。该项目使用 postgres 数据库 325 | 326 | ```bash 327 | git clone https://github.com/Haiananan/next-fast-table.git 328 | 329 | npm install 330 | cd package 331 | npm install 332 | cd .. 333 | 334 | npx prisma db push 335 | npx prisma db seed 336 | npx prisma generate 337 | npm run dev 338 | ``` 339 | 340 | ## WHY NEXT-FAST-TABLE? 341 | 342 | ### 谁适合使用它 343 | 344 | 1. 想在几分钟内搭建可用数据面板的个人开发者 345 | 2. 搭建 DEMO 或各种 MVP 服务的个人开发者或团队 346 | 3. Nextjs 个人开发者 347 | 348 | ### 开发动机 349 | 350 | 在软件开发中,后台应用开发是一个关键环节,但许多开发者对此感到厌烦。主要原因是后台应用开发通常涉及大量重复的增删改查操作和一些细小的特殊逻辑,这些重复性工作让人感觉浪费时间和精力。 351 | 352 | 我们可以将 UI 需求抽象成一个个设计组件,为什么不将后台需求也抽象成一个开箱即用的库呢?这样不仅能减少重复劳动,还能提高开发效率,使开发者专注于更具创造性的任务。 353 | 354 | #### 减法永远比加法难 355 | 356 | 目前市场上有很多现成的、完整的 admin 应用模板,这些应用提供了一整套技术栈,开发者只需运行命令就可以启动,并根据需求进行调整。然而,这些系统真的好用吗?面对一个完整的、庞大的系统,很多人感到手足无措,需要花费大量时间学习文档和阅读源码。很多初学者会认为这是自身能力的问题,但事实上,这并不是开发者自身的问题。 357 | 358 | 做减法永远比做加法难。市面上大多数系统都是完整的应用,需要开发者做减法来适应自己的需求。然而,当需求超出预设框架时,这些系统会导致大量技术负债。技术负债往往源于系统前期和后期设计目的的不同。现成的系统无法完全匹配二次开发的需求,因此许多开发者不愿意进行二次开发,最终可能面临重构甚至推倒重来的情况。 359 | 360 | #### 有的时候,有比好重要 361 | 362 | 在使用 NEXT-FAST-TABLE 之后,我可以在 1 分钟(没开玩笑:D)之内对接好一个模型所需的所有基础 CRUD 操作(排序,搜索,过滤,分页,编辑,删除等等)而我只需要细微修改逻辑,剩下的只需要定义列即可。 363 | 364 | 针对独立开发者,NEXT-FAST-TABLE 能为构建基础后台管理面板节省数小时,让开发者将更多精力投入到开发业务中。与其拿着一套完善且庞大的 admin 项目慢慢修改,不如从 0 开始构建自己需要的东西!这就是一个简单的,纯粹的,快速的,为独立开发者设身处地着想的后台管理工具组件(不至于再去使用各种数据库工具管理自己的 Saas 了) 365 | 366 | #### 原子化 367 | 368 | NEXT-FAST-TABLE 仅仅是一个针对表单的工具库,你可以将他融合进任何已有的系统中,并且自由拼接拓展。因为最繁复的表单 CRUD 内容,它已经帮你搞定了! 369 | 370 | ### 为什么要基于 Nextjs 开发 371 | 372 | 在其他的 admin 面板项目中,您不仅要定义各种列数据,还需要为网络请求操作定义很多 API,并且使用 fetch 或 axios 对接。但在 Nextjs 的 Server Actions 加持下,您可以无需对接 API,只需要定义几个函数并传入客户端组件即可工作,而且还拥有 Typescript 类型提示。当您的业务数据结构发生改变时,您只需修改列定义和相关函数,这一般会在一分钟内搞定! 373 | 374 | ### 只能使用 Server Action 吗? 375 | 376 | 不是的。你可以使用任何方式(axios,fetch...)获取数据,只要保证以规定结构返回即可。如果请求失败,需要抛出一个错误。 377 | 378 | ### 只能在 Nextjs 中使用吗? 379 | 380 | 不是的。NEXT-FAST-TABLE 是一个独立的组件,可以在任何 React 项目中使用。但是,由于它使用了 Server Action,所以在其他框架中使用时,需要自行实现数据获取。 381 | 382 | ### 为什么使用 NextUI 而不是其他 UI 库? 383 | 384 | NextUI 是一个优秀的 UI 库,提供了丰富的组件和主题,可以快速搭建页面。本项目重点关注全栈开发者的使用体验,并且提供了十分优秀的触摸反馈,适合移动端使用。我们关注于简单且极致的操作体验,而 NextUI 正是我们所需要的。不少组件库追求大而全,但忽略了细节上的打磨,比如按压触摸反馈,动画效果等。因为 NEXT-FAST-TABLE 是一个简单的快速的后端面板工具组件,所以小而美的精致 UI 是它更加需要的。 385 | 386 | ## 贡献和支持 387 | 388 | 欢迎贡献代码和提交问题。您可以在 [GitHub 仓库](https://github.com/Haiananan/next-fast-table) 提交 Pull Request 或 Issue。 389 | 390 | 本地运行项目: 391 | 392 | ```bash 393 | git clone https://github.com/Haiananan/next-fast-table.git 394 | pnpm install 395 | cd package 396 | pnpm install 397 | cd .. 398 | pnpm dev 399 | ``` 400 | 401 | 打包: 402 | 403 | ```bash 404 | cd package 405 | pnpm build 406 | ``` 407 | 408 | ## License 409 | 410 | 本项目使用 MIT 许可证。请查看 [LICENSE](./LICENSE) 文件获取更多信息。 411 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NEXT-FAST-TABLE 2 | 3 | 🦄 **NEXT-FAST-TABLE** is an atomic, out-of-the-box frontend form component for backend management applications, based on Next.js. 4 | 5 |  6 | 7 | [Engligh](./README.md) | [中文](./README.cn.md) 8 | 9 |  10 |  11 |  12 | 13 | ## Table of Contents 14 | 15 | 1. [Introduction](#introduction) 16 | 2. [Features](#features) 17 | 3. [Online Demo](#online-demo) 18 | 4. [Installation](#installation) 19 | 5. [Quick Start](#quick-start) 20 | - [Create API Program](#create-api-program) 21 | - [Define Columns and Use in Page](#define-columns-and-use-in-page) 22 | 6. [Configuration Options](#configuration-options) 23 | - [HelperConfig](#helperconfig) 24 | - [TableConfig](#tableconfig) 25 | 7. [Types](#types) 26 | 8. [Complete Example](#complete-example) 27 | 9. [WHY NEXT-FAST-TABLE](#why-next-fast-table) 28 | 10. [Contribution and Support](#contribution-and-support) 29 | 11. [License](#license) 30 | 31 | ## Introduction 32 | 33 | **NEXT-FAST-TABLE** is a powerful and efficient table component designed for developers using Next.js. It simplifies the process of displaying complex data, allowing you to quickly create and integrate tables into your application within minutes. 34 | 35 | As an independent developer, you can rapidly create a functional backend management framework similar to calling a library. It includes common CRUD operations and typical requirements like filtering and exporting. With NEXT-FAST-TABLE, you can focus more on core business logic rather than backend management. This tool enables independent developers, especially those using Next.js, to develop and deploy a functional backend management MVP in minutes rather than hours. 36 | 37 | ## Features 38 | 39 | - **🔥 Easy to Use**: Utilize Server Action for data handling without defining APIs explicitly. Alternatively, use fetch requests. 40 | - **⭐️ Rich Presets**: Easily generate forms by calling methods like Fields.string(). 41 | - **🔧 Highly Customizable**: Supports various configuration options and style customization to meet diverse application needs. 42 | - **📱 Responsive Design**: Automatically adapts to various screen sizes, providing the best user experience. 43 | - **⚙️ Advanced Data Handling**: Built-in functionalities such as sorting, filtering, pagination, and fuzzy search for seamless integration. 44 | - **📊 Comprehensive Data Types**: Supports multiple data types including text, numbers, dates, images, JSON, and Arrays. 45 | 46 | ## Online Demo 47 | 48 | 49 | DEMO 50 | 51 | 52 | ## Installation 53 | 54 | Easily install NEXT-FAST-TABLE using your preferred package manager: 55 | 56 | ```bash 57 | npm install next-fast-table 58 | ``` 59 | 60 | or 61 | 62 | ```bash 63 | yarn add next-fast-table 64 | ``` 65 | 66 | or 67 | 68 | ```bash 69 | pnpm install next-fast-table 70 | ``` 71 | 72 | ## Quick Start 73 | 74 | Here's a simple example demonstrating how to use NEXT-FAST-TABLE in a Next.js application. 75 | 76 | > Note: This is a minimal example. Refer to the full example in the project for detailed usage. 77 | 78 | ### Create API Program 79 | 80 | ```typescript 81 | "use server"; 82 | import { 83 | FetchParams, 84 | CreateParams, 85 | DeleteParams, 86 | UpdateParams, 87 | } from "next-fast-table"; 88 | 89 | // Simulated database 90 | let payments = [ 91 | { 92 | id: 1, 93 | username: "John Doe", 94 | email: "john@example.com", 95 | }, 96 | { 97 | id: 2, 98 | username: "Jane Smith", 99 | email: "jane@example.com", 100 | }, 101 | { 102 | id: 3, 103 | username: "Alice", 104 | email: "alice@example.com", 105 | }, 106 | ]; 107 | 108 | type Payment = { 109 | id: number; 110 | username: string; 111 | email: string; 112 | }; 113 | 114 | // Fetch data 115 | export async function onFetch(obj: FetchParams) { 116 | const pageSize = obj.pagination?.pageSize ?? 10; 117 | const pageIndex = obj.pagination?.pageIndex ?? 0; 118 | 119 | // Simulated sorting 120 | const sortedPayments = payments.sort((a, b) => { 121 | if (!obj.sorting || obj.sorting.length === 0) return 0; 122 | const sort = obj.sorting[0]; 123 | const multiplier = sort.desc ? -1 : 1; 124 | if (a[sort.id] < b[sort.id]) return -1 * multiplier; 125 | if (a[sort.id] > b[sort.id]) return 1 * multiplier; 126 | return 0; 127 | }); 128 | 129 | // Simulated filtering 130 | const filteredPayments = sortedPayments.filter((payment) => { 131 | if (!obj.columnFilters || obj.columnFilters.length === 0) return true; 132 | return obj.columnFilters.every((filter) => { 133 | if ( 134 | typeof filter.value === "number" || 135 | typeof filter.value === "boolean" 136 | ) { 137 | return payment[filter.id] === filter.value; 138 | } else if (typeof filter.value === "string") { 139 | return payment[filter.id].includes(filter.value); 140 | } 141 | return false; 142 | }); 143 | }); 144 | 145 | const total = filteredPayments.length; 146 | const list = filteredPayments.slice( 147 | pageIndex * pageSize, 148 | (pageIndex + 1) * pageSize 149 | ); 150 | 151 | return { 152 | list, 153 | total, 154 | }; 155 | } 156 | 157 | // Create data 158 | export async function onCreate(data: CreateParams) { 159 | payments.push(data as any); 160 | } 161 | 162 | // Delete data 163 | export async function onDelete(data: DeleteParams) { 164 | const idsToDelete = [data].flat().map((d) => d.id); 165 | payments = payments.filter((payment) => !idsToDelete.includes(payment.id)); 166 | } 167 | 168 | // Update data 169 | export async function onUpdate(data: UpdateParams) { 170 | payments = payments.map((payment) => 171 | payment.id === data.id ? { ...payment, ...data } : payment 172 | ); 173 | } 174 | ``` 175 | 176 | ### Define Columns and Use in Page 177 | 178 | ```typescript 179 | "use client"; 180 | import { NextFastTable, Fields } from "next-fast-table"; 181 | import { onCreate, onDelete, onFetch, onUpdate } from "YourAPIFile"; 182 | 183 | export default function DemoPage() { 184 | const field = Fields; 185 | 186 | const columns = [ 187 | field.number("id"), 188 | field.string("username"), 189 | field.email("email"), 190 | ]; 191 | 192 | return ( 193 | 200 | ); 201 | } 202 | ``` 203 | 204 | ## HelperConfig 205 | 206 | This configures front-end table rendering behavior. It offers multiple options to control table actions and data operations. 207 | 208 | ### Configuration Options 209 | 210 | - **input** 211 | - `disabled`: Whether input is disabled in edit mode (including create and update). Default is `false`. 212 | - `required`: Whether input is required in edit mode, participating in form validation. Default is `false`. 213 | - **list** 214 | - `hidden`: Whether the column is hidden by default. If `true`, it's not displayed by default but can be shown through column settings. Default is `false`. 215 | - **Other Options** 216 | - `label`: Label or alias for the column. Default is `undefined`. 217 | - `enableHiding`: Whether hiding is enabled. If `false`, hide button is not displayed. Default is `true`. 218 | - `enableSorting`: Whether sorting is enabled. If `false`, sorting button is not displayed. Default is `true`. 219 | - `enableColumnFilter`: Whether the column participates in column filtering. If `false`, it's not displayed in column filters. Default is `true`. 220 | - `enum`: Enum values, valid only when using `field.enum`. Default is `[]`. 221 | - `render`: Custom rendering function for display state. 222 | - Parameters: 223 | - `cell`: Value of the cell. 224 | - `row`: Row data. 225 | - Returns: JSX element or string used for rendering. 226 | 227 | ## TableConfig 228 | 229 | `TableConfig` is the prop type for the NextFastTable component, where `columns` and `onFetch` are required. 230 | 231 | ### Configuration Options 232 | 233 | - **name** 234 | - **Description**: Name of the table used for generating `tanstack-query` keys. 235 | - **Default**: `'next-table'` 236 | - **columns** 237 | - **Description**: Configuration of table columns. 238 | - **Required**: Yes 239 | - **onFetch** 240 | - **Description**: Function used to fetch table data. 241 | - **Parameters**: 242 | - `args`: Object containing pagination, sorting, and column filters. 243 | - **Returns**: Promise containing total items and data list (with ID). 244 | - **Example**: 245 | ```javascript 246 | async function fetchData({ pagination, sorting, columnFilters }) { 247 | const data = await fetchDataFromAPI({ 248 | pagination, 249 | sorting, 250 | columnFilters, 251 | }); 252 | const total = await fetchTotalCount(); 253 | return { 254 | list: data, 255 | total, 256 | }; 257 | } 258 | ``` 259 | - **onDelete** 260 | - **Description**: Function used to delete data. 261 | - **Optional**: Yes 262 | - **Parameters**: 263 | - `data`: Data to delete, can be single ID or array of IDs. 264 | - **Returns**: Promise resolved when deletion is completed. 265 | - **Example**: 266 | ```javascript 267 | async function deleteData(data) { 268 | await deleteDataFromAPI(data); 269 | } 270 | ``` 271 | - **onCreate** 272 | - **Description**: Function used to create new data. 273 | - **Optional**: Yes 274 | - **Parameters**: 275 | - `data`: Data to create. 276 | - **Returns**: Promise resolved when creation is completed. 277 | - **Example**: 278 | ```javascript 279 | async function createData(data) { 280 | const newData = await createDataInAPI(data); 281 | return newData; 282 | } 283 | ``` 284 | - **onUpdate** 285 | - **Description**: Function used to update existing data. 286 | - **Optional**: Yes 287 | - **Parameters**: 288 | - `data`: Data to update. Send only ID and fields to update. 289 | - **Returns**: Promise resolved when update is completed. 290 | - **Example**: 291 | ```javascript 292 | async function updateData(data) { 293 | const updatedData = await updateDataInAPI(data); 294 | return updatedData; 295 | } 296 | ``` 297 | 298 | ## Types 299 | 300 | ```typescript 301 | type DataWithID> = { 302 | id: number | string; 303 | } & Partial; 304 | 305 | type DataOnlyId = { 306 | id: T; 307 | }; 308 | 309 | export type FetchParams = { 310 | pagination?: { pageSize: number; pageIndex: number }; 311 | sorting?: { id: string; desc: boolean }[]; 312 | columnFilters?: { id: string; value: any }[]; 313 | }; 314 | 315 | export type DeleteParams = DataOnlyId | DataOnlyId[]; 316 | 317 | export type UpdateParams> = DataWithID; 318 | 319 | export type CreateParams> = DataWithID; 320 | ``` 321 | 322 | ## Complete Example 323 | 324 | This project is a minimal Next.js application demonstrating basic usage of NEXT-FAST-TABLE. You can run the project locally using the following steps. The project uses sqlite database, with data stored in `prisma/data.db` file. 325 | 326 | ```bash 327 | git clone https://github.com/Haiananan/next-fast-table.git 328 | 329 | npm install 330 | cd package 331 | npm install 332 | cd .. 333 | 334 | npx prisma db push 335 | npx prisma db seed 336 | npx prisma generate 337 | npm run dev 338 | ``` 339 | 340 | ## WHY NEXT-FAST-TABLE 341 | 342 | ### Who Should Use It 343 | 344 | 1. Individual developers who want to set up a functional data panel within minutes. 345 | 2. Individual developers or teams building demos or various MVP services. 346 | 3. Next.js developers. 347 | 348 | ### Development Motivation 349 | 350 | In software development, backend application development is a crucial phase, but many developers find it tedious. The primary reason is that backend application development often involves repetitive CRUD operations and some minor special logic, which can feel like a waste of time and energy. 351 | 352 | We can abstract UI requirements into design components, so why not abstract backend requirements into an out-of-the-box library? This not only reduces repetitive labor but also enhances development efficiency, allowing developers to focus on more creative tasks. 353 | 354 | #### Subtraction Is Always Harder Than Addition 355 | 356 | Currently, there are many ready-made, complete admin application templates in the market. These applications provide a full stack of technologies, allowing developers to start them with a command and adjust them according to their needs. However, are these systems really easy to use? Faced with a complete and large system, many people feel overwhelmed and need to spend a lot of time learning documentation and reading source code. Many beginners may think it's their own ability issue, but in fact, it's not the developer's own problem. 357 | 358 | Subtraction is always harder than addition. Most systems on the market are complete applications that require developers to subtract to fit their own needs. However, when the requirements exceed the preset framework, these systems can lead to a lot of technical debt. Technical debt often stems from differences in the design purposes of the system's early and late stages. Ready-made systems cannot fully match the requirements of secondary development, so many developers are unwilling to engage in secondary development, which may eventually lead to refactoring or even starting from scratch. 359 | 360 | #### Sometimes Less Is More Important Than Good 361 | 362 | After using NEXT-FAST-TABLE, I can integrate all the basic CRUD operations required by a model within 1 minute (no kidding :D), and I only need to modify the logic slightly, leaving the rest to define columns. 363 | 364 | For independent developers, NEXT-FAST-TABLE can save several hours in building basic backend management panels, allowing developers to focus more on developing business logic. Rather than slowly modifying a complete and large admin project, it's better to build what you need from scratch! This is a simple, pure, fast backend management tool component designed with empathy for independent developers (rather than using various database tools to manage their own SaaS). 365 | 366 | #### Atomic 367 | 368 | NEXT-FAST-TABLE is just a tool library for forms, you can integrate it into any existing system and freely expand it. Because the most complicated form CRUD content, it has been set up for you! 369 | 370 | ### Why Develop Based on Next.js 371 | 372 | In other admin panel projects, you not only need to define various column data but also need to define many APIs for network request operations and use fetch or axios. However, with the support of Next.js's Server Actions, you can work without having to integrate APIs, just define a few functions and pass them into client components, and also have TypeScript type hints. When your business data structure changes, you only need to modify column definitions and related functions, which can generally be completed within a minute! 373 | 374 | ### Can Only Server Actions Be Used? 375 | 376 | No. You can use any method (axios, fetch, etc.) to get data, just ensure that it returns in the specified structure. If the request fails, an error needs to be thrown. 377 | 378 | ### Can It Only Be Used in Next.js? 379 | 380 | No. NEXT-FAST-TABLE is a standalone component that can be used in any React project. However, because it uses Server Actions, when used in other frameworks, data retrieval needs to be implemented independently. 381 | 382 | ### Why Use NextUI Instead of Other UI Libraries? 383 | 384 | NextUI is an excellent UI library that provides a rich set of components and themes for quickly building pages. This project focuses on the user experience of full-stack developers and provides excellent touch feedback, suitable for mobile use. We focus on simple and ultimate operational experience, and NextUI is exactly what we need. Many component libraries pursue comprehensiveness but overlook the refinement of details, such as press touch feedback and animation effects. Because NEXT-FAST-TABLE is a simple and fast backend panel tool component, it requires a small and beautiful exquisite UI. 385 | 386 | ## Contribution and Support 387 | 388 | Contributions and issue submissions are welcome. You can submit a Pull Request or Issue on [GitHub repository](https://github.com/Haiananan/next-fast-table). 389 | 390 | Run the project locally: 391 | 392 | ```bash 393 | git clone https://github.com/Haiananan/next-fast-table.git 394 | pnpm install 395 | cd package 396 | pnpm install 397 | cd .. 398 | pnpm dev 399 | ``` 400 | 401 | Build: 402 | 403 | ```bash 404 | cd package 405 | pnpm build 406 | ``` 407 | 408 | ## License 409 | 410 | This project is licensed under the MIT License. Please see the [LICENSE](./LICENSE) file for more information. 411 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | }; 4 | 5 | module.exports = nextConfig; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-fast-table-project", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix", 9 | "seed": "prisma db seed", 10 | "postinstall": "prisma generate" 11 | }, 12 | "prisma": { 13 | "seed": "node prisma/seed.js" 14 | }, 15 | "dependencies": { 16 | "@nextui-org/react": "^2.4.5", 17 | "@prisma/client": "^5.17.0", 18 | "@react-aria/ssr": "3.9.4", 19 | "@react-aria/visually-hidden": "3.8.12", 20 | "next": "14.2.4", 21 | "prisma": "^5.17.0", 22 | "react": "18.3.1", 23 | "react-dom": "18.3.1" 24 | }, 25 | "devDependencies": { 26 | "@iconify/react": "^5.0.1", 27 | "@types/lodash": "^4.17.6", 28 | "@types/node": "20.5.7", 29 | "@types/react": "18.3.3", 30 | "@types/react-dom": "18.3.0", 31 | "@typescript-eslint/eslint-plugin": "7.2.0", 32 | "@typescript-eslint/parser": "7.2.0", 33 | "autoprefixer": "10.4.19", 34 | "eslint": "^8.57.0", 35 | "eslint-config-next": "14.2.1", 36 | "eslint-config-prettier": "^8.2.0", 37 | "eslint-plugin-import": "^2.26.0", 38 | "eslint-plugin-jsx-a11y": "^6.4.1", 39 | "eslint-plugin-node": "^11.1.0", 40 | "eslint-plugin-prettier": "^5.1.3", 41 | "eslint-plugin-react": "^7.23.2", 42 | "eslint-plugin-react-hooks": "^4.6.0", 43 | "eslint-plugin-unused-imports": "^3.2.0", 44 | "postcss": "8.4.38", 45 | "tailwind-variants": "0.1.20", 46 | "tailwindcss": "3.4.3", 47 | "typescript": "5.0.4" 48 | } 49 | } -------------------------------------------------------------------------------- /package/.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*@nextui-org/* 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /package/README.cn.md: -------------------------------------------------------------------------------- 1 | # NEXT-FAST-TABLE 2 | 3 | 🦄NEXT-FAST-TABLE 是一个原子化的,基于 Nextjs 的,开箱即用的后台管理应用前端表单组件。 4 | 5 |  6 | 7 | [Engligh](./README.md) | [中文](./README.cn.md) 8 | 9 |  10 |  11 |  12 | 13 | ## 目录 14 | 15 | 1. [介绍](#介绍) 16 | 2. [功能特点](#功能特点) 17 | 3. [在线演示](#在线演示) 18 | 4. [安装](#安装) 19 | 5. [快速开始](#快速开始) 20 | - [创建 API 程序](#创建-api-程序) 21 | - [定义列并在页面中使用](#定义列并在页面中使用) 22 | 6. [配置选项](#配置选项) 23 | - [HelperConfig](#helperconfig) 24 | - [TableConfig](#tableconfig) 25 | 7. [类型](#类型) 26 | 8. [完整案例](#完整案例) 27 | 9. [为什么选择 NEXT-FAST-TABLE](#开发动机) 28 | 10. [贡献和支持](#贡献和支持) 29 | 11. [License](#license) 30 | 31 | ## 介绍 32 | 33 | **NEXT-FAST-TABLE** 是一个强大且高效的表格组件,专为使用 Next.js 的开发者设计。它简化了复杂数据展示的过程,使您能够在一分钟内快速创建和集成表格到您的应用程序中。 34 | 35 | 作为独立开发者, 你可以像是调用库一样快速创建一个可用的后台管理框架,包含了常见的增删改查等常规行为以及过滤、导出等常见需求。通过 NEXT-FAST-TABLE, 你可以把精力更多的放在核心的业务上而不是后台管理上。这个工具能够让`独立开发者`(尤其是 Nextjs 开发者)在`数分钟内`(而不是数小时)`开发并上线`一个可用的`后台管理`MVP 36 | 37 | ## 功能特点 38 | 39 | - **🔥 易于使用**:利用 Server Action,无需定义接口,直接处理数据。当然,也可以使用 fetch 请求。 40 | - **⭐️ 预设丰富**:只需调用 Fields.string()等方法,即可生成表单 41 | - **🔧 高度可定制**:支持多种配置选项和样式自定义,满足不同应用场景的需求。 42 | - **📱 响应式设计**:自动适配各种屏幕尺寸,提供最佳用户体验。 43 | - **⚙️ 高级数据处理**:内置排序、筛选、分页、模糊搜索等功能,一键实现。 44 | - **📊 数据种类齐全**:支持多种数据类型,包括文本、数字、日期、图片等。此外,还支持 JSON 和 Array 45 | 46 | ## 在线演示 47 | 48 | 49 | DEMO 50 | 51 | 52 | ## 安装 53 | 54 | 使用你喜欢的包管理器轻松安装 NEXT-FAST-TABLE: 55 | 56 | ```bash 57 | npm install next-fast-table 58 | ``` 59 | 60 | 或 61 | 62 | ```bash 63 | yarn add next-fast-table 64 | ``` 65 | 66 | 或 67 | 68 | ```bash 69 | pnpm install next-fast-table 70 | ``` 71 | 72 | ## 快速开始 73 | 74 | 以下是一个简单的示例,展示如何在 Next.js 应用中使用 NEXT-FAST-TABLE 75 | 76 | > 注意:这只是一个最小示例,实际应用中,您可以参考本项目的完整案例。 77 | 78 | ### 创建 API 程序 79 | 80 | ```typescript 81 | "use server"; 82 | import { 83 | FetchParams, 84 | CreateParams, 85 | DeleteParams, 86 | UpdateParams, 87 | } from "next-fast-table"; 88 | 89 | // 模拟数据库 90 | let payments = [ 91 | { 92 | id: 1, 93 | username: "John Doe", 94 | email: "john@example.com", 95 | }, 96 | { 97 | id: 2, 98 | username: "Jane Smith", 99 | email: "jane@example.com", 100 | }, 101 | { 102 | id: 3, 103 | username: "Alice", 104 | email: "alice@example.com", 105 | }, 106 | ]; 107 | 108 | type Payment = { 109 | id: number; 110 | username: string; 111 | email: string; 112 | }; 113 | 114 | // 获取数据 115 | export async function onFetch(obj: FetchParams) { 116 | const pageSize = obj.pagination?.pageSize ?? 10; 117 | const pageIndex = obj.pagination?.pageIndex ?? 0; 118 | 119 | // 模拟排序 120 | const sortedPayments = payments.sort((a, b) => { 121 | if (!obj.sorting || obj.sorting.length === 0) return 0; 122 | const sort = obj.sorting[0]; 123 | const multiplier = sort.desc ? -1 : 1; 124 | if (a[sort.id] < b[sort.id]) return -1 * multiplier; 125 | if (a[sort.id] > b[sort.id]) return 1 * multiplier; 126 | return 0; 127 | }); 128 | 129 | // 模拟过滤 130 | const filteredPayments = sortedPayments.filter((payment) => { 131 | if (!obj.columnFilters || obj.columnFilters.length === 0) return true; 132 | return obj.columnFilters.every((filter) => { 133 | if ( 134 | typeof filter.value === "number" || 135 | typeof filter.value === "boolean" 136 | ) { 137 | return payment[filter.id] === filter.value; 138 | } else if (typeof filter.value === "string") { 139 | return payment[filter.id].includes(filter.value); 140 | } 141 | return false; 142 | }); 143 | }); 144 | 145 | const total = filteredPayments.length; 146 | const list = filteredPayments.slice( 147 | pageIndex * pageSize, 148 | (pageIndex + 1) * pageSize 149 | ); 150 | 151 | return { 152 | list, 153 | total, 154 | }; 155 | } 156 | 157 | // 创建数据 158 | export async function onCreate(data: CreateParams) { 159 | payments.push(data as any); 160 | } 161 | 162 | // 删除数据 163 | export async function onDelete(data: DeleteParams) { 164 | const idsToDelete = [data].flat().map((d) => d.id); 165 | payments = payments.filter((payment) => !idsToDelete.includes(payment.id)); 166 | } 167 | 168 | // 更新数据 169 | export async function onUpdate(data: UpdateParams) { 170 | payments = payments.map((payment) => 171 | payment.id === data.id ? { ...payment, ...data } : payment 172 | ); 173 | } 174 | ``` 175 | 176 | ### 定义列并在页面中使用 177 | 178 | ```typescript 179 | "use client"; 180 | import { NextFastTable, Fields } from "next-fast-table"; 181 | import { onCreate, onDelete, onFetch, onUpdate } from "YourAPIFile"; 182 | 183 | export default function DemoPage() { 184 | const field = Fields; 185 | 186 | const columns = [ 187 | field.number("id"), 188 | field.string("username"), 189 | field.email("email"), 190 | ]; 191 | 192 | return ( 193 | 200 | ); 201 | } 202 | ``` 203 | 204 | ## HelperConfig 205 | 206 | 这是一个用于控制前端表格渲染的配置选项。它提供了多种选项来控制表格的行为和数据操作。 207 | 208 | ### 配置选项 209 | 210 | - **input** 211 | - `disabled`: 在编辑模式下(包括创建和编辑)输入是否禁用。默认值为 `false`。 212 | - `required`: 在编辑模式下输入是否必填,参与表单验证。默认值为 `false`。 213 | - **list** 214 | - `hidden`: 列是否默认隐藏。如果为 `true`,则默认不显示,但可以通过列设置显示。默认值为 `false`。 215 | - **其他选项** 216 | - `label`: 列的标签或别名。默认值为 `undefined`。 217 | - `enableHiding`: 是否允许隐藏。如果为 `false`,则不显示隐藏按钮。默认值为 `true`。 218 | - `enableSorting`: 是否允许排序。如果为 `false`,则不显示排序按钮。默认值为 `true`。 219 | - `enableColumnFilter`: 列是否参与列过滤。如果为 `false`,则不显示在列过滤中。默认值为 `true`。 220 | - `enum`: 枚举值,仅在使用 `field.enum` 时有效。默认值为 `[]`。 221 | - `render`: 用于在显示状态下自定义渲染的自定义渲染函数。 222 | - 参数: 223 | - `cell`: 单元格的值。 224 | - `row`: 行数据。 225 | - 返回值: 用于渲染的 JSX 元素或字符串。 226 | 227 | ## TableConfig 228 | 229 | `TableConfig` 是 NextFastTable 组件的传参类型,其中 `columns` 和 `onFetch` 是必填的。 230 | 231 | ### 配置选项 232 | 233 | - **name** 234 | - **描述**: 表格的名称,用于生成 `tanstack-query` 的键。 235 | - **默认值**: `'next-table'` 236 | - **columns** 237 | - **描述**: 表格的列配置。 238 | - **必填**: 是 239 | - **onFetch** 240 | - **描述**: 用于获取表格数据的函数。 241 | - **参数**: 242 | - `args`: 包含分页、排序和列过滤器的对象。 243 | - **返回值**: 一个包含总项目数和数据列表(带 ID)的 Promise。 244 | - **示例**: 245 | ```javascript 246 | async function fetchData({ pagination, sorting, columnFilters }) { 247 | const data = await fetchDataFromAPI({ 248 | pagination, 249 | sorting, 250 | columnFilters, 251 | }); 252 | const total = await fetchTotalCount(); 253 | return { 254 | list: data, 255 | total, 256 | }; 257 | } 258 | ``` 259 | - **onDelete** 260 | - **描述**: 用于删除数据的函数。 261 | - **可选**: 是 262 | - **参数**: 263 | - `data`: 要删除的数据,可以是单个 ID 或 ID 数组。 264 | - **返回值**: 一个在删除完成时解析的 Promise。 265 | - **示例**: 266 | ```javascript 267 | async function deleteData(data) { 268 | await deleteDataFromAPI(data); 269 | } 270 | ``` 271 | - **onCreate** 272 | - **描述**: 用于创建新数据的函数。 273 | - **可选**: 是 274 | - **参数**: 275 | - `data`: 要创建的数据。 276 | - **返回值**: 一个在创建完成时解析的 Promise。 277 | - **示例**: 278 | ```javascript 279 | async function createData(data) { 280 | const newData = await createDataInAPI(data); 281 | return newData; 282 | } 283 | ``` 284 | - **onUpdate** 285 | - **描述**: 用于更新现有数据的函数。 286 | - **可选**: 是 287 | - **参数**: 288 | - `data`: 要更新的数据。仅发送 ID 和要更新的字段。 289 | - **返回值**: 一个在更新完成时解析的 Promise。 290 | - **示例**: 291 | ```javascript 292 | async function updateData(data) { 293 | const updatedData = await updateDataInAPI(data); 294 | return updatedData; 295 | } 296 | ``` 297 | 298 | ## 类型 299 | 300 | ```typescript 301 | type DataWithID> = { 302 | id: number | string; 303 | } & Partial; 304 | 305 | type DataOnlyId = { 306 | id: T; 307 | }; 308 | 309 | export type FetchParams = { 310 | pagination?: { pageSize: number; pageIndex: number }; 311 | sorting?: { id: string; desc: boolean }[]; 312 | columnFilters?: { id: string; value: any }[]; 313 | }; 314 | 315 | export type DeleteParams = DataOnlyId | DataOnlyId[]; 316 | 317 | export type UpdateParams> = DataWithID; 318 | 319 | export type CreateParams> = DataWithID; 320 | ``` 321 | 322 | ## 完整案例 323 | 324 | 本项目是一个最小化的 Next.js 应用,用于演示 NEXT-FAST-TABLE 的基本用法。您可以通过以下步骤在本地运行该项目。该项目使用 postgres 数据库 325 | 326 | ```bash 327 | git clone https://github.com/Haiananan/next-fast-table.git 328 | 329 | npm install 330 | cd package 331 | npm install 332 | cd .. 333 | 334 | npx prisma db push 335 | npx prisma db seed 336 | npx prisma generate 337 | npm run dev 338 | ``` 339 | 340 | ## WHY NEXT-FAST-TABLE? 341 | 342 | ### 谁适合使用它 343 | 344 | 1. 想在几分钟内搭建可用数据面板的个人开发者 345 | 2. 搭建 DEMO 或各种 MVP 服务的个人开发者或团队 346 | 3. Nextjs 个人开发者 347 | 348 | ### 开发动机 349 | 350 | 在软件开发中,后台应用开发是一个关键环节,但许多开发者对此感到厌烦。主要原因是后台应用开发通常涉及大量重复的增删改查操作和一些细小的特殊逻辑,这些重复性工作让人感觉浪费时间和精力。 351 | 352 | 我们可以将 UI 需求抽象成一个个设计组件,为什么不将后台需求也抽象成一个开箱即用的库呢?这样不仅能减少重复劳动,还能提高开发效率,使开发者专注于更具创造性的任务。 353 | 354 | #### 减法永远比加法难 355 | 356 | 目前市场上有很多现成的、完整的 admin 应用模板,这些应用提供了一整套技术栈,开发者只需运行命令就可以启动,并根据需求进行调整。然而,这些系统真的好用吗?面对一个完整的、庞大的系统,很多人感到手足无措,需要花费大量时间学习文档和阅读源码。很多初学者会认为这是自身能力的问题,但事实上,这并不是开发者自身的问题。 357 | 358 | 做减法永远比做加法难。市面上大多数系统都是完整的应用,需要开发者做减法来适应自己的需求。然而,当需求超出预设框架时,这些系统会导致大量技术负债。技术负债往往源于系统前期和后期设计目的的不同。现成的系统无法完全匹配二次开发的需求,因此许多开发者不愿意进行二次开发,最终可能面临重构甚至推倒重来的情况。 359 | 360 | #### 有的时候,有比好重要 361 | 362 | 在使用 NEXT-FAST-TABLE 之后,我可以在 1 分钟(没开玩笑:D)之内对接好一个模型所需的所有基础 CRUD 操作(排序,搜索,过滤,分页,编辑,删除等等)而我只需要细微修改逻辑,剩下的只需要定义列即可。 363 | 364 | 针对独立开发者,NEXT-FAST-TABLE 能为构建基础后台管理面板节省数小时,让开发者将更多精力投入到开发业务中。与其拿着一套完善且庞大的 admin 项目慢慢修改,不如从 0 开始构建自己需要的东西!这就是一个简单的,纯粹的,快速的,为独立开发者设身处地着想的后台管理工具组件(不至于再去使用各种数据库工具管理自己的 Saas 了) 365 | 366 | #### 原子化 367 | 368 | NEXT-FAST-TABLE 仅仅是一个针对表单的工具库,你可以将他融合进任何已有的系统中,并且自由拼接拓展。因为最繁复的表单 CRUD 内容,它已经帮你搞定了! 369 | 370 | ### 为什么要基于 Nextjs 开发 371 | 372 | 在其他的 admin 面板项目中,您不仅要定义各种列数据,还需要为网络请求操作定义很多 API,并且使用 fetch 或 axios 对接。但在 Nextjs 的 Server Actions 加持下,您可以无需对接 API,只需要定义几个函数并传入客户端组件即可工作,而且还拥有 Typescript 类型提示。当您的业务数据结构发生改变时,您只需修改列定义和相关函数,这一般会在一分钟内搞定! 373 | 374 | ### 只能使用 Server Action 吗? 375 | 376 | 不是的。你可以使用任何方式(axios,fetch...)获取数据,只要保证以规定结构返回即可。如果请求失败,需要抛出一个错误。 377 | 378 | ### 只能在 Nextjs 中使用吗? 379 | 380 | 不是的。NEXT-FAST-TABLE 是一个独立的组件,可以在任何 React 项目中使用。但是,由于它使用了 Server Action,所以在其他框架中使用时,需要自行实现数据获取。 381 | 382 | ### 为什么使用 NextUI 而不是其他 UI 库? 383 | 384 | NextUI 是一个优秀的 UI 库,提供了丰富的组件和主题,可以快速搭建页面。本项目重点关注全栈开发者的使用体验,并且提供了十分优秀的触摸反馈,适合移动端使用。我们关注于简单且极致的操作体验,而 NextUI 正是我们所需要的。不少组件库追求大而全,但忽略了细节上的打磨,比如按压触摸反馈,动画效果等。因为 NEXT-FAST-TABLE 是一个简单的快速的后端面板工具组件,所以小而美的精致 UI 是它更加需要的。 385 | 386 | ## 贡献和支持 387 | 388 | 欢迎贡献代码和提交问题。您可以在 [GitHub 仓库](https://github.com/Haiananan/next-fast-table) 提交 Pull Request 或 Issue。 389 | 390 | 本地运行项目: 391 | 392 | ```bash 393 | git clone https://github.com/Haiananan/next-fast-table.git 394 | pnpm install 395 | cd package 396 | pnpm install 397 | cd .. 398 | pnpm dev 399 | ``` 400 | 401 | 打包: 402 | 403 | ```bash 404 | cd package 405 | pnpm build 406 | ``` 407 | 408 | ## License 409 | 410 | 本项目使用 MIT 许可证。请查看 [LICENSE](./LICENSE) 文件获取更多信息。 411 | -------------------------------------------------------------------------------- /package/README.md: -------------------------------------------------------------------------------- 1 | # NEXT-FAST-TABLE 2 | 3 | 🦄 **NEXT-FAST-TABLE** is an atomic, out-of-the-box frontend form component for backend management applications, based on Next.js. 4 | 5 |  6 | 7 | [Engligh](./README.md) | [中文](./README.cn.md) 8 | 9 |  10 |  11 |  12 | 13 | ## Table of Contents 14 | 15 | 1. [Introduction](#introduction) 16 | 2. [Features](#features) 17 | 3. [Online Demo](#online-demo) 18 | 4. [Installation](#installation) 19 | 5. [Quick Start](#quick-start) 20 | - [Create API Program](#create-api-program) 21 | - [Define Columns and Use in Page](#define-columns-and-use-in-page) 22 | 6. [Configuration Options](#configuration-options) 23 | - [HelperConfig](#helperconfig) 24 | - [TableConfig](#tableconfig) 25 | 7. [Types](#types) 26 | 8. [Complete Example](#complete-example) 27 | 9. [WHY NEXT-FAST-TABLE](#why-next-fast-table) 28 | 10. [Contribution and Support](#contribution-and-support) 29 | 11. [License](#license) 30 | 31 | ## Introduction 32 | 33 | **NEXT-FAST-TABLE** is a powerful and efficient table component designed for developers using Next.js. It simplifies the process of displaying complex data, allowing you to quickly create and integrate tables into your application within minutes. 34 | 35 | As an independent developer, you can rapidly create a functional backend management framework similar to calling a library. It includes common CRUD operations and typical requirements like filtering and exporting. With NEXT-FAST-TABLE, you can focus more on core business logic rather than backend management. This tool enables independent developers, especially those using Next.js, to develop and deploy a functional backend management MVP in minutes rather than hours. 36 | 37 | ## Features 38 | 39 | - **🔥 Easy to Use**: Utilize Server Action for data handling without defining APIs explicitly. Alternatively, use fetch requests. 40 | - **⭐️ Rich Presets**: Easily generate forms by calling methods like Fields.string(). 41 | - **🔧 Highly Customizable**: Supports various configuration options and style customization to meet diverse application needs. 42 | - **📱 Responsive Design**: Automatically adapts to various screen sizes, providing the best user experience. 43 | - **⚙️ Advanced Data Handling**: Built-in functionalities such as sorting, filtering, pagination, and fuzzy search for seamless integration. 44 | - **📊 Comprehensive Data Types**: Supports multiple data types including text, numbers, dates, images, JSON, and Arrays. 45 | 46 | ## Online Demo 47 | 48 | 49 | DEMO 50 | 51 | 52 | ## Installation 53 | 54 | Easily install NEXT-FAST-TABLE using your preferred package manager: 55 | 56 | ```bash 57 | npm install next-fast-table 58 | ``` 59 | 60 | or 61 | 62 | ```bash 63 | yarn add next-fast-table 64 | ``` 65 | 66 | or 67 | 68 | ```bash 69 | pnpm install next-fast-table 70 | ``` 71 | 72 | ## Quick Start 73 | 74 | Here's a simple example demonstrating how to use NEXT-FAST-TABLE in a Next.js application. 75 | 76 | > Note: This is a minimal example. Refer to the full example in the project for detailed usage. 77 | 78 | ### Create API Program 79 | 80 | ```typescript 81 | "use server"; 82 | import { 83 | FetchParams, 84 | CreateParams, 85 | DeleteParams, 86 | UpdateParams, 87 | } from "next-fast-table"; 88 | 89 | // Simulated database 90 | let payments = [ 91 | { 92 | id: 1, 93 | username: "John Doe", 94 | email: "john@example.com", 95 | }, 96 | { 97 | id: 2, 98 | username: "Jane Smith", 99 | email: "jane@example.com", 100 | }, 101 | { 102 | id: 3, 103 | username: "Alice", 104 | email: "alice@example.com", 105 | }, 106 | ]; 107 | 108 | type Payment = { 109 | id: number; 110 | username: string; 111 | email: string; 112 | }; 113 | 114 | // Fetch data 115 | export async function onFetch(obj: FetchParams) { 116 | const pageSize = obj.pagination?.pageSize ?? 10; 117 | const pageIndex = obj.pagination?.pageIndex ?? 0; 118 | 119 | // Simulated sorting 120 | const sortedPayments = payments.sort((a, b) => { 121 | if (!obj.sorting || obj.sorting.length === 0) return 0; 122 | const sort = obj.sorting[0]; 123 | const multiplier = sort.desc ? -1 : 1; 124 | if (a[sort.id] < b[sort.id]) return -1 * multiplier; 125 | if (a[sort.id] > b[sort.id]) return 1 * multiplier; 126 | return 0; 127 | }); 128 | 129 | // Simulated filtering 130 | const filteredPayments = sortedPayments.filter((payment) => { 131 | if (!obj.columnFilters || obj.columnFilters.length === 0) return true; 132 | return obj.columnFilters.every((filter) => { 133 | if ( 134 | typeof filter.value === "number" || 135 | typeof filter.value === "boolean" 136 | ) { 137 | return payment[filter.id] === filter.value; 138 | } else if (typeof filter.value === "string") { 139 | return payment[filter.id].includes(filter.value); 140 | } 141 | return false; 142 | }); 143 | }); 144 | 145 | const total = filteredPayments.length; 146 | const list = filteredPayments.slice( 147 | pageIndex * pageSize, 148 | (pageIndex + 1) * pageSize 149 | ); 150 | 151 | return { 152 | list, 153 | total, 154 | }; 155 | } 156 | 157 | // Create data 158 | export async function onCreate(data: CreateParams) { 159 | payments.push(data as any); 160 | } 161 | 162 | // Delete data 163 | export async function onDelete(data: DeleteParams) { 164 | const idsToDelete = [data].flat().map((d) => d.id); 165 | payments = payments.filter((payment) => !idsToDelete.includes(payment.id)); 166 | } 167 | 168 | // Update data 169 | export async function onUpdate(data: UpdateParams) { 170 | payments = payments.map((payment) => 171 | payment.id === data.id ? { ...payment, ...data } : payment 172 | ); 173 | } 174 | ``` 175 | 176 | ### Define Columns and Use in Page 177 | 178 | ```typescript 179 | "use client"; 180 | import { NextFastTable, Fields } from "next-fast-table"; 181 | import { onCreate, onDelete, onFetch, onUpdate } from "YourAPIFile"; 182 | 183 | export default function DemoPage() { 184 | const field = Fields; 185 | 186 | const columns = [ 187 | field.number("id"), 188 | field.string("username"), 189 | field.email("email"), 190 | ]; 191 | 192 | return ( 193 | 200 | ); 201 | } 202 | ``` 203 | 204 | ## HelperConfig 205 | 206 | This configures front-end table rendering behavior. It offers multiple options to control table actions and data operations. 207 | 208 | ### Configuration Options 209 | 210 | - **input** 211 | - `disabled`: Whether input is disabled in edit mode (including create and update). Default is `false`. 212 | - `required`: Whether input is required in edit mode, participating in form validation. Default is `false`. 213 | - **list** 214 | - `hidden`: Whether the column is hidden by default. If `true`, it's not displayed by default but can be shown through column settings. Default is `false`. 215 | - **Other Options** 216 | - `label`: Label or alias for the column. Default is `undefined`. 217 | - `enableHiding`: Whether hiding is enabled. If `false`, hide button is not displayed. Default is `true`. 218 | - `enableSorting`: Whether sorting is enabled. If `false`, sorting button is not displayed. Default is `true`. 219 | - `enableColumnFilter`: Whether the column participates in column filtering. If `false`, it's not displayed in column filters. Default is `true`. 220 | - `enum`: Enum values, valid only when using `field.enum`. Default is `[]`. 221 | - `render`: Custom rendering function for display state. 222 | - Parameters: 223 | - `cell`: Value of the cell. 224 | - `row`: Row data. 225 | - Returns: JSX element or string used for rendering. 226 | 227 | ## TableConfig 228 | 229 | `TableConfig` is the prop type for the NextFastTable component, where `columns` and `onFetch` are required. 230 | 231 | ### Configuration Options 232 | 233 | - **name** 234 | - **Description**: Name of the table used for generating `tanstack-query` keys. 235 | - **Default**: `'next-table'` 236 | - **columns** 237 | - **Description**: Configuration of table columns. 238 | - **Required**: Yes 239 | - **onFetch** 240 | - **Description**: Function used to fetch table data. 241 | - **Parameters**: 242 | - `args`: Object containing pagination, sorting, and column filters. 243 | - **Returns**: Promise containing total items and data list (with ID). 244 | - **Example**: 245 | ```javascript 246 | async function fetchData({ pagination, sorting, columnFilters }) { 247 | const data = await fetchDataFromAPI({ 248 | pagination, 249 | sorting, 250 | columnFilters, 251 | }); 252 | const total = await fetchTotalCount(); 253 | return { 254 | list: data, 255 | total, 256 | }; 257 | } 258 | ``` 259 | - **onDelete** 260 | - **Description**: Function used to delete data. 261 | - **Optional**: Yes 262 | - **Parameters**: 263 | - `data`: Data to delete, can be single ID or array of IDs. 264 | - **Returns**: Promise resolved when deletion is completed. 265 | - **Example**: 266 | ```javascript 267 | async function deleteData(data) { 268 | await deleteDataFromAPI(data); 269 | } 270 | ``` 271 | - **onCreate** 272 | - **Description**: Function used to create new data. 273 | - **Optional**: Yes 274 | - **Parameters**: 275 | - `data`: Data to create. 276 | - **Returns**: Promise resolved when creation is completed. 277 | - **Example**: 278 | ```javascript 279 | async function createData(data) { 280 | const newData = await createDataInAPI(data); 281 | return newData; 282 | } 283 | ``` 284 | - **onUpdate** 285 | - **Description**: Function used to update existing data. 286 | - **Optional**: Yes 287 | - **Parameters**: 288 | - `data`: Data to update. Send only ID and fields to update. 289 | - **Returns**: Promise resolved when update is completed. 290 | - **Example**: 291 | ```javascript 292 | async function updateData(data) { 293 | const updatedData = await updateDataInAPI(data); 294 | return updatedData; 295 | } 296 | ``` 297 | 298 | ## Types 299 | 300 | ```typescript 301 | type DataWithID> = { 302 | id: number | string; 303 | } & Partial; 304 | 305 | type DataOnlyId = { 306 | id: T; 307 | }; 308 | 309 | export type FetchParams = { 310 | pagination?: { pageSize: number; pageIndex: number }; 311 | sorting?: { id: string; desc: boolean }[]; 312 | columnFilters?: { id: string; value: any }[]; 313 | }; 314 | 315 | export type DeleteParams = DataOnlyId | DataOnlyId[]; 316 | 317 | export type UpdateParams> = DataWithID; 318 | 319 | export type CreateParams> = DataWithID; 320 | ``` 321 | 322 | ## Complete Example 323 | 324 | This project is a minimal Next.js application demonstrating basic usage of NEXT-FAST-TABLE. You can run the project locally using the following steps. The project uses sqlite database, with data stored in `prisma/data.db` file. 325 | 326 | ```bash 327 | git clone https://github.com/Haiananan/next-fast-table.git 328 | 329 | npm install 330 | cd package 331 | npm install 332 | cd .. 333 | 334 | npx prisma db push 335 | npx prisma db seed 336 | npx prisma generate 337 | npm run dev 338 | ``` 339 | 340 | ## WHY NEXT-FAST-TABLE 341 | 342 | ### Who Should Use It 343 | 344 | 1. Individual developers who want to set up a functional data panel within minutes. 345 | 2. Individual developers or teams building demos or various MVP services. 346 | 3. Next.js developers. 347 | 348 | ### Development Motivation 349 | 350 | In software development, backend application development is a crucial phase, but many developers find it tedious. The primary reason is that backend application development often involves repetitive CRUD operations and some minor special logic, which can feel like a waste of time and energy. 351 | 352 | We can abstract UI requirements into design components, so why not abstract backend requirements into an out-of-the-box library? This not only reduces repetitive labor but also enhances development efficiency, allowing developers to focus on more creative tasks. 353 | 354 | #### Subtraction Is Always Harder Than Addition 355 | 356 | Currently, there are many ready-made, complete admin application templates in the market. These applications provide a full stack of technologies, allowing developers to start them with a command and adjust them according to their needs. However, are these systems really easy to use? Faced with a complete and large system, many people feel overwhelmed and need to spend a lot of time learning documentation and reading source code. Many beginners may think it's their own ability issue, but in fact, it's not the developer's own problem. 357 | 358 | Subtraction is always harder than addition. Most systems on the market are complete applications that require developers to subtract to fit their own needs. However, when the requirements exceed the preset framework, these systems can lead to a lot of technical debt. Technical debt often stems from differences in the design purposes of the system's early and late stages. Ready-made systems cannot fully match the requirements of secondary development, so many developers are unwilling to engage in secondary development, which may eventually lead to refactoring or even starting from scratch. 359 | 360 | #### Sometimes Less Is More Important Than Good 361 | 362 | After using NEXT-FAST-TABLE, I can integrate all the basic CRUD operations required by a model within 1 minute (no kidding :D), and I only need to modify the logic slightly, leaving the rest to define columns. 363 | 364 | For independent developers, NEXT-FAST-TABLE can save several hours in building basic backend management panels, allowing developers to focus more on developing business logic. Rather than slowly modifying a complete and large admin project, it's better to build what you need from scratch! This is a simple, pure, fast backend management tool component designed with empathy for independent developers (rather than using various database tools to manage their own SaaS). 365 | 366 | #### Atomic 367 | 368 | NEXT-FAST-TABLE is just a tool library for forms, you can integrate it into any existing system and freely expand it. Because the most complicated form CRUD content, it has been set up for you! 369 | 370 | ### Why Develop Based on Next.js 371 | 372 | In other admin panel projects, you not only need to define various column data but also need to define many APIs for network request operations and use fetch or axios. However, with the support of Next.js's Server Actions, you can work without having to integrate APIs, just define a few functions and pass them into client components, and also have TypeScript type hints. When your business data structure changes, you only need to modify column definitions and related functions, which can generally be completed within a minute! 373 | 374 | ### Can Only Server Actions Be Used? 375 | 376 | No. You can use any method (axios, fetch, etc.) to get data, just ensure that it returns in the specified structure. If the request fails, an error needs to be thrown. 377 | 378 | ### Can It Only Be Used in Next.js? 379 | 380 | No. NEXT-FAST-TABLE is a standalone component that can be used in any React project. However, because it uses Server Actions, when used in other frameworks, data retrieval needs to be implemented independently. 381 | 382 | ### Why Use NextUI Instead of Other UI Libraries? 383 | 384 | NextUI is an excellent UI library that provides a rich set of components and themes for quickly building pages. This project focuses on the user experience of full-stack developers and provides excellent touch feedback, suitable for mobile use. We focus on simple and ultimate operational experience, and NextUI is exactly what we need. Many component libraries pursue comprehensiveness but overlook the refinement of details, such as press touch feedback and animation effects. Because NEXT-FAST-TABLE is a simple and fast backend panel tool component, it requires a small and beautiful exquisite UI. 385 | 386 | ## Contribution and Support 387 | 388 | Contributions and issue submissions are welcome. You can submit a Pull Request or Issue on [GitHub repository](https://github.com/Haiananan/next-fast-table). 389 | 390 | Run the project locally: 391 | 392 | ```bash 393 | git clone https://github.com/Haiananan/next-fast-table.git 394 | pnpm install 395 | cd package 396 | pnpm install 397 | cd .. 398 | pnpm dev 399 | ``` 400 | 401 | Build: 402 | 403 | ```bash 404 | cd package 405 | pnpm build 406 | ``` 407 | 408 | ## License 409 | 410 | This project is licensed under the MIT License. Please see the [LICENSE](./LICENSE) file for more information. 411 | -------------------------------------------------------------------------------- /package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-fast-table", 3 | "version": "1.0.9", 4 | "description": "A super-fast form building solution for full-stack developers, build your data forms in one minute.", 5 | "main": "dist/index.js", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "keywords": [ 9 | "next", 10 | "nexttable", 11 | "table", 12 | "next fast table", 13 | "nextui", 14 | "next table server action", 15 | "react", 16 | "react table", 17 | "react fast table", 18 | "next admin" 19 | ], 20 | "scripts": { 21 | "build": "tsup" 22 | }, 23 | "files": [ 24 | "dist" 25 | ], 26 | "author": "blaine", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/Haiananan/next-fast-table.git" 31 | }, 32 | "dependencies": { 33 | "@iconify/react": "^5.0.1", 34 | "@internationalized/date": "^3.5.4", 35 | "@nextui-org/react": "*", 36 | "@tanstack/react-query": "^5.51.8", 37 | "@tanstack/react-table": "^8.19.3", 38 | "framer-motion": "^11.3.8", 39 | "react": "^18.3.1", 40 | "react-dom": "^18.3.1", 41 | "react-hook-form": "^7.52.1", 42 | "react-json-pretty": "^2.2.0", 43 | "react-use": "^17.5.0", 44 | "sonner": "^1.5.0", 45 | "ua-parser-js": "^1.0.38" 46 | }, 47 | "devDependencies": { 48 | "@babel/core": "^7.24.9", 49 | "@babel/preset-env": "^7.24.8", 50 | "@babel/preset-react": "^7.24.7", 51 | "@babel/preset-typescript": "^7.24.7", 52 | "@types/react": "^18.3.3", 53 | "@types/react-dom": "^18.3.0", 54 | "@types/ua-parser-js": "^0.7.39", 55 | "autoprefixer": "^10.0.0", 56 | "babel-loader": "^9.1.3", 57 | "postcss": "^8.0.0", 58 | "tailwindcss": "^2.0.0", 59 | "tsup": "^5.0.0", 60 | "typescript": "^4.9.5", 61 | "webpack": "^5.93.0", 62 | "webpack-cli": "^5.1.4", 63 | "webpack-dev-server": "^5.0.4" 64 | } 65 | } -------------------------------------------------------------------------------- /package/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /package/public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haiananan/next-fast-table/11c86b1fafb944e31fab24f5a41249bc38a15221/package/public/image.png -------------------------------------------------------------------------------- /package/src/DataTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from "react"; 2 | import type { 3 | ColumnFiltersState, 4 | ColumnOrderState, 5 | ColumnPinningState, 6 | PaginationState, 7 | RowSelectionState, 8 | SortingState, 9 | VisibilityState, 10 | } from "@tanstack/react-table"; 11 | import { 12 | getCoreRowModel, 13 | getFilteredRowModel, 14 | getPaginationRowModel, 15 | getSortedRowModel, 16 | useReactTable, 17 | } from "@tanstack/react-table"; 18 | import { useMutation, useQuery } from "@tanstack/react-query"; 19 | import { toast } from "sonner"; 20 | import { Icon } from "@iconify/react"; 21 | import { useMedia } from "react-use"; 22 | import { Controller, useForm } from "react-hook-form"; 23 | import { getLocalTimeZone, fromDate } from "@internationalized/date"; 24 | import { typedIcon } from "./helper"; 25 | import { MyTableBody } from "./TableBody"; 26 | import { 27 | useDisclosure, 28 | Modal, 29 | ModalBody, 30 | ModalContent, 31 | ModalFooter, 32 | ModalHeader, 33 | Button, 34 | Input, 35 | Textarea, 36 | Select, 37 | SelectItem, 38 | DatePicker, 39 | Pagination, 40 | Dropdown, 41 | DropdownItem, 42 | DropdownMenu, 43 | DropdownTrigger, 44 | } from "@nextui-org/react"; 45 | 46 | type DataWithID> = { 47 | id: number | string; 48 | } & Partial; 49 | 50 | type DataOnlyId = { 51 | id: T; 52 | }; 53 | 54 | export type FetchParams = { 55 | pagination: PaginationState; 56 | columnFilters: ColumnFiltersState; 57 | sorting: SortingState; 58 | }; 59 | 60 | export type DeleteParams = DataOnlyId | DataOnlyId[]; 61 | 62 | export type UpdateParams> = DataWithID; 63 | 64 | export type CreateParams> = DataWithID; 65 | 66 | /** 67 | * Table configuration options. 68 | */ 69 | export interface TableConfig { 70 | /** 71 | * The name of the table, used to generate the key for tanstack-query. Defaults to 'next-table'. 72 | */ 73 | name?: string; 74 | 75 | /** 76 | * The columns configuration for the table. 77 | */ 78 | columns: any; 79 | 80 | /** 81 | * Function to fetch data for the table. 82 | * @param args - The fetch arguments including pagination, column filters, and sorting state. 83 | * @returns A promise that resolves with the total number of items and the list of data with ID. 84 | * @example 85 | * async function fetchData({ pagination, sorting, columnFilters }) { 86 | * const data = await fetchDataFromAPI({ 87 | * pagination, 88 | * sorting, 89 | * columnFilters, 90 | * }); 91 | * const total = await fetchTotalCount(); 92 | * return { 93 | * list: data, 94 | * total, 95 | * }; 96 | * } 97 | */ 98 | onFetch: ( 99 | args: FetchParams 100 | ) => Promise<{ total: number; list: DataWithID[] }>; 101 | 102 | /** 103 | * Function to delete data. 104 | * @param data - The data to be deleted, either a single ID or an array of IDs. 105 | * @returns A promise that resolves when the deletion is complete. 106 | * @example 107 | * async function deleteData(data) { 108 | * await deleteDataFromAPI(data); 109 | * } 110 | */ 111 | onDelete?: (data: any) => Promise; 112 | 113 | /** 114 | * Function to create new data. 115 | * @param data - The data to be created. 116 | * @returns A promise that resolves when the creation is complete. 117 | * @example 118 | * async function createData(data) { 119 | * const newData = await createDataInAPI(data); 120 | * return newData; 121 | * } 122 | */ 123 | onCreate?: (data: any) => Promise; 124 | 125 | /** 126 | * Function to update existing data. 127 | * @param data - The data to be updated. Only the ID and fields to be updated will be sent. 128 | * @returns A promise that resolves when the update is complete. 129 | * @example 130 | * async function updateData(data) { 131 | * const updatedData = await updateDataInAPI(data); 132 | * return updatedData; 133 | * } 134 | */ 135 | onUpdate?: (data: any) => Promise; 136 | } 137 | export function DataTable({ 138 | name = "next-table", 139 | columns, 140 | onFetch, 141 | onDelete, 142 | onCreate, 143 | onUpdate, 144 | }: TableConfig) { 145 | const initalHiddenColumns = columns 146 | .filter((col) => col?.meta?.list?.hidden) 147 | .map((item) => item.accessorKey) 148 | .reduce((acc, cur) => ({ ...acc, [cur]: false }), {}); 149 | const [sorting, setSorting] = useState([]); 150 | const [columnFilters, setColumnFilters] = useState([]); 151 | const [columnVisibility, setColumnVisibility] = 152 | useState(initalHiddenColumns); 153 | const [rowSelection, setRowSelection] = useState({}); 154 | const [columnOrder, setColumnOrder] = useState([]); 155 | const [columnPinning, setColumnPinning] = useState({}); 156 | const [pagination, setPagination] = useState({ 157 | pageSize: 20, 158 | pageIndex: 0, 159 | }); 160 | const [data, setData] = useState([]); 161 | const [pageCount, setPageCount] = useState(1); 162 | const [total, setTotal] = useState(0); 163 | 164 | const table = useReactTable({ 165 | pageCount, 166 | data, 167 | columns, 168 | manualPagination: true, 169 | manualSorting: true, 170 | manualFiltering: true, 171 | getCoreRowModel: getCoreRowModel(), 172 | getPaginationRowModel: getPaginationRowModel(), 173 | getSortedRowModel: getSortedRowModel(), 174 | getFilteredRowModel: getFilteredRowModel(), 175 | onColumnFiltersChange: setColumnFilters, 176 | onSortingChange: setSorting, 177 | onColumnVisibilityChange: setColumnVisibility, 178 | onRowSelectionChange: setRowSelection, 179 | onPaginationChange: setPagination, 180 | onColumnOrderChange: setColumnOrder, 181 | onColumnPinningChange: setColumnPinning, 182 | state: { 183 | sorting, 184 | columnFilters, 185 | columnVisibility, 186 | rowSelection, 187 | pagination, 188 | columnOrder, 189 | columnPinning, 190 | }, 191 | }); 192 | 193 | const isMobile = useMedia("(max-width: 768px)", true); 194 | 195 | const getQuery = useQuery({ 196 | queryKey: [name, { sorting, columnFilters, pagination }], 197 | queryFn: () => onFetch({ pagination, columnFilters, sorting }), 198 | }); 199 | 200 | const deleteMutation = useMutation({ 201 | mutationFn: onDelete, 202 | onSuccess: (data, variables) => { 203 | toast.success("Row deleted successfully"); 204 | getQuery.refetch(); 205 | 206 | onClose(); 207 | }, 208 | onError: (err, variables) => { 209 | toast.error("Failed to delete rows", { description: err.message }); 210 | }, 211 | }); 212 | 213 | const updateMutation = useMutation({ 214 | mutationFn: onUpdate, 215 | onSuccess: () => { 216 | toast.success("Row updated successfully"); 217 | getQuery.refetch(); 218 | onClose(); 219 | }, 220 | onError: (e) => { 221 | toast.error("Failed to update row", { description: e.message }); 222 | }, 223 | }); 224 | 225 | const createMutation = useMutation({ 226 | mutationFn: onCreate, 227 | onSuccess: (data, variables) => { 228 | toast.success("Row created successfully"); 229 | getQuery.refetch(); 230 | onClose(); 231 | }, 232 | onError: (e) => { 233 | toast.error("Failed to create row", { description: e.message }); 234 | }, 235 | }); 236 | 237 | useEffect(() => { 238 | if (getQuery.isSuccess) { 239 | setPageCount(Math.ceil(getQuery.data.total / pagination.pageSize) ?? 1); 240 | setData((getQuery.data?.list as any) ?? []); 241 | setTotal(getQuery.data?.total ?? 0); 242 | } 243 | }, [getQuery.isSuccess, getQuery.data, pagination.pageSize]); 244 | 245 | const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure({ 246 | onClose() { 247 | setMode(undefined); 248 | reset({}); 249 | }, 250 | }); 251 | // const [targetRow, setTargetRow] = useState({}); 252 | const [mode, setMode] = useState< 253 | "create" | "edit" | "filter" | "view" | "delete" | undefined 254 | >(undefined); 255 | const { 256 | control, 257 | handleSubmit, 258 | formState: { dirtyFields, isDirty, defaultValues }, 259 | register, 260 | reset, 261 | getValues, 262 | setValue, 263 | watch, 264 | } = useForm({}); 265 | // it will slow down the input 266 | const [formData, setFormData] = useState({}); 267 | useEffect(() => { 268 | if (process.env.NODE_ENV === "development") { 269 | const formData = watch(); 270 | setFormData(formData); 271 | } 272 | }, []); 273 | 274 | const onSubmit = (data: any) => { 275 | const dirtyData = Object.keys(dirtyFields).reduce((acc, cur) => { 276 | return { ...acc, [cur]: data[cur] }; 277 | }, {}); 278 | 279 | const dirtyDataWithPrimaryKey = { 280 | ...dirtyData, 281 | [primaryKey]: data[primaryKey], 282 | }; 283 | 284 | console.log({ dirtyDataWithPrimaryKey, dirtyData, data, primaryKey }); 285 | 286 | if (mode === "create") { 287 | createMutation.mutate(dirtyData as any); 288 | } else if (mode === "edit") { 289 | updateMutation.mutate(dirtyDataWithPrimaryKey as any); 290 | } else if (mode === "filter") { 291 | const arr = Object.entries(dirtyData) 292 | .filter(([key, value]) => value !== undefined && value !== "") 293 | .map(([key, value]) => ({ id: key, value })); 294 | 295 | setColumnFilters(arr); 296 | onClose(); 297 | } else if (mode === "view") { 298 | navigator.clipboard.writeText(JSON.stringify(data)); 299 | toast.success("Copied to clipboard"); 300 | onClose(); 301 | } else if (mode === "delete") { 302 | deleteMutation.mutate({ [primaryKey]: data[primaryKey] }); 303 | } else { 304 | console.log("No mode selected"); 305 | toast.error("No mode selected"); 306 | } 307 | }; 308 | 309 | const visibleColumnIds = table 310 | .getVisibleFlatColumns() 311 | .map((column) => column.id); 312 | const isFilterDirty = 313 | table 314 | .getState() 315 | .columnFilters.filter( 316 | (item) => item.id && item.value !== undefined && item.value !== "" 317 | ).length > 0; 318 | 319 | const onCreateButtonClick = () => { 320 | setMode("create"); 321 | reset({}); 322 | onOpen(); 323 | }; 324 | 325 | const onResetButtonClick = () => { 326 | table.resetColumnFilters(); 327 | onClose(); 328 | }; 329 | 330 | const onDeleteButtonClick = () => { 331 | const rows = table.getSelectedRowModel().rows; 332 | const items = rows.map((row) => ({ 333 | id: row.original.id, 334 | })); 335 | table.resetRowSelection(); 336 | deleteMutation.mutate(items as any); 337 | }; 338 | 339 | // const onTableSelectionChange = (value) => { 340 | // if (value === "all") return table.toggleAllRowsSelected(); 341 | 342 | // table.setRowSelection( 343 | // Array.from(value).reduce((acc, cur) => ({ ...acc, [+cur]: true }), {}) 344 | // ); 345 | // }; 346 | 347 | const isCreateOrEditMode = mode === "create" || mode === "edit"; 348 | 349 | const primaryKey = "id"; 350 | 351 | const inputDefaultValue = (key: string): any | undefined => { 352 | if (mode === "edit" || mode === "view") { 353 | // return targetRow[key]; 354 | return getValues(key); 355 | } else if (mode === "create") { 356 | return undefined; 357 | } else if (mode === "filter") { 358 | const filter = table 359 | .getState() 360 | .columnFilters.find((item) => item.id === key); 361 | if (filter !== undefined) { 362 | return filter.value; 363 | } 364 | } 365 | return undefined; 366 | }; 367 | 368 | const iconClasses = 369 | "text-xl text-default-500 pointer-events-none flex-shrink-0"; 370 | 371 | const memoizedTable = useMemo( 372 | () => ( 373 | 382 | ), 383 | [table, isMobile, getQuery] 384 | ); 385 | 386 | return ( 387 | 388 | {/* 392 | {Object.entries({ 393 | defaultValues, 394 | formData, 395 | isDirty, 396 | dirtyFields, 397 | mode, 398 | columnFilters, 399 | }).map(([key, value]) => ( 400 | 401 | {key} 402 | {JSON.stringify(value, null, 2)} 403 | 404 | ))} 405 | */} 406 | 417 | 418 | 419 | {mode} 420 | 421 | 422 | 423 | {columns.map((column) => ( 424 | 425 | 426 | {["string", "number", "longtext"].includes( 427 | column.meta?.type 428 | ) && ( 429 | { 449 | if (mode !== "view") return; 450 | navigator.clipboard.writeText( 451 | getValues(column.accessorKey) 452 | ); 453 | toast.success("Copied to clipboard"); 454 | }} 455 | className={column.enableColumnFilter ? "" : "hidden"} 456 | endContent={typedIcon(column.meta?.type)} 457 | label={column.header} 458 | isReadOnly={mode === "view"} 459 | isDisabled={ 460 | column.meta?.input?.disabled && isCreateOrEditMode 461 | } 462 | isRequired={ 463 | column.meta?.input?.required && mode !== "filter" 464 | } 465 | /> 466 | )} 467 | 468 | {column.meta?.type === "boolean" && ( 469 | ( 473 | 481 | ) => { 482 | const select = e.target.value; 483 | if (select === "true") { 484 | return field.onChange(true); 485 | } 486 | if (select === "false") { 487 | return field.onChange(false); 488 | } 489 | return field.onChange(undefined); 490 | }} 491 | > 492 | {["false", "true"].map((str) => ( 493 | 494 | {str} 495 | 496 | ))} 497 | 498 | )} 499 | /> 500 | )} 501 | {column.meta?.type === "date" && ( 502 | ( 506 | { 515 | field.onChange(date.toDate()); 516 | }} 517 | isDisabled={column.meta?.edit?.disabled} 518 | isRequired={ 519 | column.meta?.edit?.required && isCreateOrEditMode 520 | } 521 | /> 522 | )} 523 | /> 524 | )} 525 | {column.meta?.type === "enum" && ( 526 | ( 530 | 536 | ) => field.onChange(e.target.value)} 537 | > 538 | {column.meta?.enum?.map((value) => ( 539 | 540 | {value} 541 | 542 | ))} 543 | 544 | )} 545 | /> 546 | )} 547 | {["array", "json"].includes(column.meta?.type) && ( 548 | ( 552 | { 563 | try { 564 | field.onChange(JSON.parse(e.target.value)); 565 | } catch (error) { 566 | field.onChange(e.target.value); 567 | } 568 | }} 569 | onClick={() => { 570 | if (mode !== "view") return; 571 | navigator.clipboard.writeText( 572 | JSON.stringify( 573 | getValues(column.accessorKey), 574 | null, 575 | 2 576 | ) 577 | ); 578 | toast.success("Copied to clipboard"); 579 | }} 580 | className={ 581 | column.enableColumnFilter ? "" : "hidden" 582 | } 583 | label={column.header} 584 | isReadOnly={mode === "view"} 585 | isDisabled={ 586 | column.meta?.input?.disabled && isCreateOrEditMode 587 | } 588 | isRequired={ 589 | column.meta?.input?.required && mode !== "filter" 590 | } 591 | /> 592 | )} 593 | /> 594 | )} 595 | 596 | 597 | ))} 598 | 599 | 600 | 601 | 605 | {mode === "filter" ? "Reset" : "Cancel"} 606 | 607 | 618 | 619 | {mode && mode === "view" ? "Copy" : mode} 620 | 621 | 622 | 623 | 624 | 625 | 629 | getQuery.refetch()} 637 | startContent={} 638 | > 639 | {isMobile ? undefined : "Refresh"} 640 | 641 | 642 | 643 | 644 | } 651 | > 652 | {isMobile ? undefined : "Columns"} 653 | 654 | 655 | 664 | {table 665 | .getAllColumns() 666 | .filter( 667 | (column) => 668 | typeof column.accessorFn !== "undefined" && 669 | column.getCanHide() 670 | ) 671 | .map((column: any) => { 672 | return ( 673 | column.toggleVisibility()} 675 | key={column.id} 676 | > 677 | 678 | {typedIcon(column.columnDef.meta?.type)} 679 | 680 | {column.columnDef.header} 681 | 682 | 683 | 684 | ); 685 | })} 686 | 687 | 688 | { 690 | setMode("filter"); 691 | // setTargetRow({}); 692 | reset({}); 693 | onOpen(); 694 | }} 695 | size={isMobile ? "lg" : undefined} 696 | isIconOnly={isMobile} 697 | className=" flex-shrink-0 mr-auto" 698 | color="primary" 699 | variant={isFilterDirty ? "ghost" : "solid"} 700 | startContent={} 701 | > 702 | {isMobile ? undefined : "Filter"} 703 | 704 | 705 | 706 | 707 | {onDelete && ( 708 | } 716 | onClick={onDeleteButtonClick} 717 | isDisabled={table.getSelectedRowModel().rows.length === 0 || false} 718 | > 719 | {isMobile ? undefined : "Delete"} 720 | 721 | )} 722 | {onCreate && ( 723 | } 730 | onClick={onCreateButtonClick} 731 | > 732 | {isMobile ? undefined : "Create"} 733 | 734 | )} 735 | 736 | 737 | 738 | {memoizedTable} 739 | 740 | 741 | 790 | 791 | ); 792 | } 793 | -------------------------------------------------------------------------------- /package/src/TableBody.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | cn, 4 | Table, 5 | TableBody, 6 | TableCell, 7 | TableHeader, 8 | TableRow, 9 | TableColumn, 10 | Spinner, 11 | Button, 12 | Dropdown, 13 | DropdownTrigger, 14 | DropdownMenu, 15 | DropdownItem, 16 | } from "@nextui-org/react"; 17 | import { flexRender } from "@tanstack/react-table"; 18 | import { Icon } from "@iconify/react"; 19 | 20 | export function MyTableBody({ 21 | table, 22 | getQuery, 23 | setMode, 24 | reset, 25 | onOpen, 26 | hideDelete, 27 | hideEdit, 28 | }) { 29 | const iconClasses = 30 | "text-xl text-default-500 pointer-events-none flex-shrink-0"; 31 | 32 | return ( 33 | { 40 | if (value === "all") return table.toggleAllRowsSelected(); 41 | table.setRowSelection( 42 | Array.from(value).reduce((acc, cur) => ({ ...acc, [+cur]: true }), {}) 43 | ); 44 | }} 45 | aria-label="data-table" 46 | selectedKeys={table.getSelectedRowModel().rows.map((row) => row.id)} 47 | sortDescriptor={{ 48 | column: table.getState().sorting[0]?.id, 49 | direction: table.getState().sorting[0]?.desc 50 | ? "descending" 51 | : "ascending", 52 | }} 53 | onSortChange={({ column, direction }) => { 54 | table.getColumn(column as string)?.toggleSorting(); 55 | }} 56 | > 57 | 58 | {table.getHeaderGroups()[0].headers.map((header) => ( 59 | 64 | {header.isPlaceholder 65 | ? null 66 | : flexRender(header.column.columnDef.header, header.getContext())} 67 | 68 | ))} 69 | 70 | actions 71 | 72 | 73 | } 77 | items={table.getRowModel().rows} 78 | > 79 | {table.getRowModel().rows.map((row) => ( 80 | 81 | {row.getVisibleCells().map((cell) => ( 82 | 83 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 84 | 85 | ))} 86 | 87 | 88 | 89 | 90 | Actions 91 | 92 | 93 | 97 | {!hideEdit && 98 | (( 99 | 108 | } 109 | onPress={() => { 110 | setMode("edit"); 111 | reset(row.original as any); 112 | onOpen(); 113 | }} 114 | > 115 | Edit 116 | 117 | ) as any)} 118 | 124 | } 125 | onClick={() => { 126 | setMode("view"); 127 | reset(row.original as any); 128 | onOpen(); 129 | }} 130 | > 131 | View 132 | 133 | {!hideDelete && ( 134 | 144 | } 145 | onClick={() => { 146 | setMode("delete"); 147 | reset(row.original as any); 148 | onOpen(); 149 | }} 150 | > 151 | Delete 152 | 153 | )} 154 | 155 | 156 | 157 | 158 | ))} 159 | 160 | 161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /package/src/helper.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from "@iconify/react"; 2 | import React from "react"; 3 | import { Button, Chip, Spacer, Tooltip, Image, Link } from "@nextui-org/react"; 4 | import JSONPretty from "react-json-pretty"; 5 | import "react-json-pretty/themes/monikai.css"; 6 | import { UAParser } from "ua-parser-js"; 7 | 8 | export function parseDateRange(obj: any) { 9 | try { 10 | // 解析 JSON 字符串为对象 11 | const dateRange = obj; 12 | 13 | // 提取 start 和 end 日期对象 14 | const start = dateRange.start; 15 | const end = dateRange.end; 16 | 17 | // 将 JSON 日期对象转换为 JavaScript Date 对象 18 | const fromDate = new Date( 19 | start.year, 20 | start.month - 1, // month is 0-indexed in JavaScript Date 21 | start.day 22 | ); 23 | const toDate = new Date( 24 | end.year, 25 | end.month - 1, // month is 0-indexed in JavaScript Date 26 | end.day 27 | ); 28 | 29 | // 返回结果对象 30 | return { 31 | from: fromDate, 32 | to: toDate, 33 | }; 34 | } catch (error) { 35 | console.error("Invalid JSON string:", error); 36 | return { 37 | from: new Date(), 38 | to: new Date(), 39 | }; 40 | } 41 | } 42 | export function typedIcon(type: string) { 43 | const iconType = iconMap[type] || "default-icon"; 44 | return ; 45 | } 46 | const iconMap = { 47 | string: "material-symbols:text-fields-rounded", 48 | number: "f7:number", 49 | date: "material-symbols:date-range", 50 | boolean: "oui:token-boolean", 51 | array: "material-symbols:list-alt", 52 | json: "material-symbols:code", 53 | longtext: "material-symbols:article", 54 | enum: "material-symbols:label", 55 | }; 56 | 57 | function typedCell(type: string, cell: any) { 58 | if (type === "number") { 59 | return cell ? {+cell.toString()} : N/A; 60 | } 61 | if (type === "date") { 62 | return cell ? ( 63 | {new Date(cell).toLocaleDateString()} 64 | ) : ( 65 | N/A 66 | ); 67 | } 68 | 69 | if (type === "boolean") { 70 | return ( 71 | 72 | {cell ? ( 73 | 77 | ) : ( 78 | 82 | )} 83 | 84 | ); 85 | } 86 | 87 | if (type === "array" || type === "json") { 88 | return ( 89 | 92 | 96 | 97 | } 98 | > 99 | 100 | {Array.isArray(cell) ? `Array(${cell.length})` : `Object`} 101 | 102 | 103 | ); 104 | } 105 | 106 | if (type === "enum") { 107 | const specialStates = { 108 | success: [ 109 | "succeeded", 110 | "successed", 111 | "done", 112 | "completed", 113 | "finish", 114 | "finished", 115 | "success", 116 | "pass", 117 | "passed", 118 | "approve", 119 | "approved", 120 | "accept", 121 | "accepted", 122 | ], 123 | warning: [ 124 | "waiting", 125 | "warning", 126 | "warn", 127 | "pending", 128 | "hold", 129 | "holded", 130 | "holdup", 131 | "holduped", 132 | "delay", 133 | "delayed", 134 | "process", 135 | "processing", 136 | ], 137 | danger: [ 138 | "fail", 139 | "failed", 140 | "failure", 141 | "canceled", 142 | "cancelled", 143 | "reject", 144 | "rejected", 145 | "deny", 146 | "denied", 147 | "refuse", 148 | "refused", 149 | "block", 150 | "blocked", 151 | "stop", 152 | "stopped", 153 | "halt", 154 | "halted", 155 | "pause", 156 | "paused", 157 | "suspend", 158 | "suspended", 159 | "inactive", 160 | "invalid", 161 | "illegal", 162 | "unauthorized", 163 | "forbidden", 164 | "notallowed", 165 | ], 166 | }; 167 | 168 | let color = "default"; 169 | for (const state in specialStates) { 170 | if (specialStates[state].includes(cell)) { 171 | color = state; 172 | break; 173 | } 174 | } 175 | 176 | return ( 177 | 178 | {cell} 179 | 180 | ); 181 | } 182 | 183 | return ( 184 | 185 | {cell} 186 | 187 | ); 188 | } 189 | 190 | /** 191 | * Configuration options for helper functions. 192 | */ 193 | interface HelperConfig { 194 | /** 195 | * Configuration for the input state. 196 | */ 197 | input?: { 198 | /** 199 | * Whether the input is disabled in edit mode, including both creation and editing. 200 | */ 201 | disabled?: boolean; 202 | /** 203 | * Whether the input is required in edit mode, participating in form validation. 204 | */ 205 | required?: boolean; 206 | }; 207 | /** 208 | * Configuration for the display state. 209 | */ 210 | list?: { 211 | /** 212 | * Whether the column is hidden by default. If true, it will not be displayed by default, but can be shown through column settings. 213 | */ 214 | hidden?: boolean; 215 | }; 216 | /** 217 | * The label or alias for the column. 218 | */ 219 | label?: string; 220 | /** 221 | * Whether hiding is allowed. If false, the hide button will not be displayed. 222 | */ 223 | enableHiding?: boolean; 224 | /** 225 | * Whether sorting is allowed. If false, the sort button will not be displayed. 226 | */ 227 | enableSorting?: boolean; 228 | /** 229 | * Whether the column participates in column filtering. If false, it will not be displayed in the column filter. 230 | */ 231 | enableColumnFilter?: boolean; 232 | /** 233 | * Enumeration values, only effective when using `field.enum`. 234 | */ 235 | enum?: string[]; 236 | /** 237 | * Custom render function for custom rendering in display state. 238 | * @param cell - The cell value. 239 | * @param row - The row data. 240 | * @returns A JSX element or string for rendering. 241 | */ 242 | render?: ({ cell, row }: { cell: any; row: any }) => JSX.Element | string; 243 | } 244 | 245 | function helper(metaType: string, hconfig = {}) { 246 | /** 247 | * @param key - Column identifier used to access data 248 | * @example key: "id" "amount" "profile.name" "a.b.c.d.e" 249 | * @param config - Configuration options 250 | * @example config: { label: "UID", input: { required: true }, render: ({ cell, row }) => {cell} } 251 | */ 252 | return (key: string, config: HelperConfig = hconfig) => { 253 | return { 254 | meta: { 255 | type: metaType, 256 | input: { 257 | disabled: config.input?.disabled ?? false, 258 | required: config.input?.required ?? false, 259 | }, 260 | list: { 261 | hidden: config.list?.hidden ?? false, 262 | }, 263 | enum: config.enum ?? [], 264 | }, 265 | accessorKey: key, 266 | header: config.label ?? key, 267 | enableHiding: config.enableHiding ?? true, 268 | enableSorting: config.enableSorting ?? true, 269 | enableColumnFilter: config.enableColumnFilter ?? true, 270 | cell: ({ cell, row }) => { 271 | return config.render 272 | ? config.render({ cell: cell.getValue(), row: row.original }) 273 | : typedCell(metaType, cell.getValue()); 274 | }, 275 | }; 276 | }; 277 | } 278 | const uaParser = new UAParser(); 279 | 280 | export const Fields = { 281 | string: helper("string"), 282 | number: helper("number"), 283 | boolean: helper("boolean"), 284 | date: helper("date"), 285 | array: helper("array"), 286 | json: helper("json"), 287 | longtext: helper("longtext"), 288 | enum: helper("enum"), 289 | ua: helper("string", { 290 | render: ({ cell }) => { 291 | const parser = new UAParser(); 292 | parser.setUA(cell); 293 | const result = parser.getResult(); 294 | 295 | const browserIcon = 296 | { 297 | Chrome: "teenyicons:chrome-solid", 298 | Firefox: "ri:firefox-fill", 299 | Safari: "fa6-brands:safari", 300 | "Mobile Safari": "fa6-brands:safari", 301 | Edge: "mdi:microsoft-edge", 302 | Opera: "mdi:opera", 303 | "Internet Explorer": "mdi:internet-explorer", 304 | }[result.browser.name as any] || "mdi:web"; 305 | 306 | const osIcon = 307 | { 308 | Windows: "mdi:microsoft-windows", 309 | "Mac OS": "mdi:apple", 310 | iOS: "mdi:apple", 311 | Android: "mdi:android", 312 | Linux: "mdi:linux", 313 | Ubuntu: "cib:ubuntu", 314 | }[result.os.name as any] || "mingcute:device-fill"; 315 | 316 | return ( 317 | 318 | } 320 | color="primary" 321 | variant="flat" 322 | > 323 | {result.os.name} {result.os.version ? result.os.version : ""} 324 | 325 | 326 | } 328 | color="secondary" 329 | variant="flat" 330 | > 331 | {result.browser.name}{" "} 332 | {result.browser.version ? result.browser.version : ""} 333 | 334 | 335 | ); 336 | }, 337 | }), 338 | image: helper("string", { 339 | render: ({ cell }) => ( 340 | 341 | { 349 | // open image in new tab 350 | window.open(cell, "_blank"); 351 | }} 352 | /> 353 | 354 | ), 355 | }), 356 | email: helper("string", { 357 | render: ({ cell }) => ( 358 | 359 | {cell} 360 | 361 | ), 362 | }), 363 | tag: helper("string", { 364 | render: ({ cell }) => ( 365 | 366 | {cell} 367 | 368 | ), 369 | }), 370 | link: helper("string", { 371 | render: ({ cell }) => ( 372 | 377 | {cell} 378 | 379 | ), 380 | }), 381 | ip: helper("string", { 382 | render: ({ cell }) => {cell}, 383 | }), 384 | }; 385 | -------------------------------------------------------------------------------- /package/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DataTable, 3 | TableConfig, 4 | UpdateParams, 5 | FetchParams, 6 | CreateParams, 7 | DeleteParams, 8 | } from "./DataTable"; 9 | import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; 10 | import React from "react"; 11 | import "./tailwind.css"; 12 | import { Fields } from "./helper"; 13 | import { Toaster } from "sonner"; 14 | 15 | const queryClient = new QueryClient(); 16 | 17 | function NextFastTable(props: TableConfig) { 18 | return ( 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | export { 26 | NextFastTable, 27 | Fields, 28 | type FetchParams, 29 | type CreateParams, 30 | type UpdateParams, 31 | type DeleteParams, 32 | }; 33 | -------------------------------------------------------------------------------- /package/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /package/tailwind.config.mjs: -------------------------------------------------------------------------------- 1 | import {nextui} from '@nextui-org/theme' 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [ 6 | './src/**/*.{js,ts,jsx,tsx,mdx}', 7 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}' 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | darkMode: "class", 13 | plugins: [nextui()], 14 | } 15 | -------------------------------------------------------------------------------- /package/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", // 目标 JavaScript 版本 4 | "module": "ESNext", // 使用 ESNext 模块系统 5 | "declaration": true, // 生成 .d.ts 文件 6 | "outDir": "./dist", // 输出目录 7 | "strict": true, // 启用所有严格类型检查选项 8 | "esModuleInterop": true, // 启用 ES 模块交互 9 | "skipLibCheck": true, // 跳过库文件检查 10 | "forceConsistentCasingInFileNames": true, // 强制文件名一致 11 | "jsx": "react", 12 | "moduleResolution": "node", // 使用 node 模块解析 13 | "noImplicitAny": false 14 | }, 15 | "include": ["src"], // 包含的文件 16 | "exclude": ["node_modules", "dist"] // 排除的文件 17 | } -------------------------------------------------------------------------------- /package/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.tsx"], 5 | format: ["cjs", "esm"], 6 | dts: true, 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | outDir: "dist", 11 | injectStyle: true, 12 | esbuildOptions(options) { 13 | options.banner = { 14 | js: '"use client"', 15 | } 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgres" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Payment { 14 | id Int @id @default(autoincrement()) 15 | createdAt DateTime @default(now()) 16 | updatedAt DateTime @updatedAt 17 | amount Int? 18 | username String? 19 | avatar String? 20 | currency String @default("USD") 21 | status String @default("pending") 22 | email String? 23 | phone String? 24 | isChecked Boolean @default(false) 25 | ip String? 26 | ua String? 27 | referer String? 28 | tags String[] @default([]) 29 | extra Json? 30 | } 31 | -------------------------------------------------------------------------------- /prisma/seed.js: -------------------------------------------------------------------------------- 1 | //seed 2 | 3 | const { PrismaClient } = require("@prisma/client"); 4 | const prisma = new PrismaClient(); 5 | const datas = require("./data.json"); 6 | 7 | function randomStringArray(length) { 8 | const arr = []; 9 | for (let i = 0; i < length; i++) { 10 | arr.push(Math.random().toString(36).substring(7)); 11 | } 12 | return arr; 13 | } 14 | 15 | function randomJSON(length) { 16 | const obj = {}; 17 | for (let i = 0; i < length; i++) { 18 | obj[Math.random().toString(36).substring(7)] = Math.random() 19 | .toString(36) 20 | .substring(7); 21 | } 22 | return obj; 23 | } 24 | 25 | async function main() { 26 | // You can change this, a seed action will create 500 payments, starting from id 0, 27 | // if you want to add more, just change the START_ID, example 501, 1001, etc 28 | const START_ID = 0; 29 | 30 | const paymentData = []; 31 | for (let i = START_ID; i < START_ID + 500; i++) { 32 | const data = datas[i % 500]; 33 | paymentData.push({ 34 | ...data, 35 | id: i, 36 | tags: randomStringArray(5), 37 | extra: randomJSON(5), 38 | }); 39 | } 40 | 41 | const createdPayments = await prisma.payment.createMany({ 42 | data: paymentData, 43 | }); 44 | 45 | console.log(`Created ${createdPayments.count} payments`); 46 | } 47 | 48 | main() 49 | .catch((e) => { 50 | console.error(e.message); 51 | }) 52 | .finally(async () => { 53 | await prisma.$disconnect(); 54 | }); 55 | -------------------------------------------------------------------------------- /public/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haiananan/next-fast-table/11c86b1fafb944e31fab24f5a41249bc38a15221/public/image.png -------------------------------------------------------------------------------- /src/actions/payment.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { Payment } from "@prisma/client"; 3 | import prisma from "@/lib/db"; 4 | import { 5 | FetchParams, 6 | CreateParams, 7 | DeleteParams, 8 | UpdateParams, 9 | } from "../../package/src/index"; 10 | 11 | function isDate(obj: any) { 12 | return obj instanceof Date && !isNaN(obj as any); 13 | } 14 | 15 | export async function onFetch(obj: FetchParams) { 16 | const pageSize = obj.pagination?.pageSize ?? 10; 17 | const pageIndex = obj.pagination?.pageIndex ?? 0; 18 | 19 | const sorting = 20 | obj.sorting?.map((sort) => { 21 | return { [sort.id]: sort.desc ? "desc" : "asc" }; 22 | }) || []; 23 | 24 | const filters = 25 | obj.columnFilters?.map((filter) => { 26 | if ( 27 | typeof filter.value === "number" || 28 | typeof filter.value === "boolean" 29 | ) { 30 | return { 31 | [filter.id]: { 32 | equals: filter.value, 33 | }, 34 | }; 35 | } else if (typeof filter.value === "string") { 36 | return { [filter.id]: { contains: filter.value } }; 37 | } else if (isDate(filter.value)) { 38 | return { 39 | [filter.id]: { 40 | gte: filter.value, 41 | }, 42 | }; 43 | } 44 | }) || []; 45 | 46 | const total = await prisma.payment.count({ 47 | where: { 48 | AND: filters as any, 49 | }, 50 | }); 51 | 52 | const payments = await prisma.payment.findMany({ 53 | take: pageSize, 54 | skip: pageIndex * pageSize, 55 | orderBy: sorting.length > 0 ? sorting : [{ id: "desc" }], 56 | where: { 57 | AND: filters as any, 58 | }, 59 | }); 60 | 61 | return { 62 | list: payments, 63 | total, 64 | }; 65 | } 66 | 67 | export async function onCreate(data: CreateParams) { 68 | await prisma.payment.create({ 69 | data: data as any, 70 | }); 71 | } 72 | 73 | export async function onDelete(data: DeleteParams) { 74 | await prisma.payment.deleteMany({ 75 | where: { 76 | id: { 77 | in: [data].flat().map((d) => d.id), 78 | }, 79 | }, 80 | }); 81 | } 82 | 83 | export async function onUpdate(data: UpdateParams) { 84 | await prisma.payment.update({ 85 | where: { 86 | id: data.id, 87 | }, 88 | data: data as any, 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Layout({ children }) { 4 | return ( 5 | 6 | {children} 7 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { NextFastTable, Fields } from "../../package/src/index"; 3 | import { onCreate, onDelete, onFetch, onUpdate } from "@/actions/payment"; 4 | const currencyEnum = [ 5 | "USD", // United States Dollar 6 | "EUR", // Euro 7 | "JPY", // Japanese Yen 8 | "GBP", // British Pound Sterling 9 | "AUD", // Australian Dollar 10 | "CAD", // Canadian Dollar 11 | "CHF", // Swiss Franc 12 | "CNY", // Chinese Yuan 13 | "SEK", // Swedish Krona 14 | "NZD", // New Zealand Dollar 15 | "MXN", // Mexican Peso 16 | "SGD", // Singapore Dollar 17 | "HKD", // Hong Kong Dollar 18 | "NOK", // Norwegian Krone 19 | "KRW", // South Korean Won 20 | "TRY", // Turkish Lira 21 | "RUB", // Russian Ruble 22 | "INR", // Indian Rupee 23 | "BRL", // Brazilian Real 24 | "ZAR", // South African Rand 25 | ]; 26 | 27 | export default function DemoPage() { 28 | const field = Fields; 29 | 30 | const columns = [ 31 | field.number("id"), 32 | field.image("avatar"), 33 | field.string("username"), 34 | field.number("amount"), 35 | field.enum("currency", { enum: currencyEnum }), 36 | field.email("email"), 37 | field.enum("status", { enum: ["pending", "success", "failure"] }), 38 | field.boolean("isChecked"), 39 | field.ip("ip"), 40 | field.ua("ua"), 41 | field.date("createdAt"), 42 | field.date("updatedAt"), 43 | field.link("referer"), 44 | field.array("tags"), 45 | field.json("extra"), 46 | ]; 47 | 48 | 49 | return ( 50 | 51 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined 5 | } 6 | 7 | const db = globalThis.prisma || new PrismaClient() 8 | export default db 9 | 10 | if (process.env.NODE_ENV !== 'production') globalThis.prisma = db 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import {nextui} from '@nextui-org/theme' 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [ 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}' 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | darkMode: "class", 14 | plugins: [nextui()], 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "downlevelIteration": true, 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "noImplicitAny": false, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": ["./src/*"] 26 | } 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | --------------------------------------------------------------------------------
{JSON.stringify(value, null, 2)}
619 | {mode && mode === "view" ? "Copy" : mode} 620 |