@_tc/template-core 0.0.1-bate.57 → 0.0.1-bate.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.skills/tc-component-usage-skills/SKILL.md +5 -0
- package/.skills/tc-component-usage-skills/reference/component-api.md +3 -0
- package/.skills/tc-generator/reference/example.md +4 -0
- package/.skills/tc-generator/reference/project-template/config/config.default.js +4 -0
- package/AGENT_README.md +38 -9
- package/README.md +82 -2
- package/cjs/config/config.default.js +1 -1
- package/cjs/packages/core/index.js +1 -1
- package/cjs/packages/core/loader/middleware.js +1 -1
- package/esm/app/{middleware → middlewares}/api-params-verify.js +1 -1
- package/esm/app/{middleware → middlewares}/api-sign-verify.js +1 -1
- package/esm/app/{middleware → middlewares}/error-handle.js +1 -1
- package/esm/app/{middleware → middlewares}/project-handler.js +1 -1
- package/esm/app/{middleware → middlewares}/request-parameter-parsing.js +1 -1
- package/esm/config/config.default.js +4 -0
- package/esm/packages/core/index.js +37 -33
- package/esm/packages/core/loader/middleware.js +1 -1
- package/fe/frontend/src/common/request.js +1 -1
- package/fe/packages/react/ui/components/Input/Input.js +93 -39
- package/fe/packages/react/ui/components/testPage/index.js +29 -3
- package/package.json +7 -2
- package/types/config/config.default.d.ts +27 -1
- package/types/packages/core/loader/config.d.ts +1 -1
- /package/cjs/app/{middleware → middlewares}/api-params-verify.js +0 -0
- /package/cjs/app/{middleware → middlewares}/api-sign-verify.js +0 -0
- /package/cjs/app/{middleware → middlewares}/error-handle.js +0 -0
- /package/cjs/app/{middleware → middlewares}/project-handler.js +0 -0
- /package/cjs/app/{middleware → middlewares}/request-parameter-parsing.js +0 -0
- /package/types/app/{middleware → middlewares}/api-params-verify.d.ts +0 -0
- /package/types/app/{middleware → middlewares}/api-sign-verify.d.ts +0 -0
- /package/types/app/{middleware → middlewares}/error-handle.d.ts +0 -0
- /package/types/app/{middleware → middlewares}/project-handler.d.ts +0 -0
- /package/types/app/{middleware → middlewares}/request-parameter-parsing.d.ts +0 -0
|
@@ -31,8 +31,13 @@ Every input-like component supports both modes. Check for `value !== undefined`
|
|
|
31
31
|
|
|
32
32
|
// Uncontrolled
|
|
33
33
|
<Input defaultValue="hello" />
|
|
34
|
+
|
|
35
|
+
// Password fields get a built-in show/hide toggle
|
|
36
|
+
<Input type="password" value={password} onChange={setPassword} />
|
|
34
37
|
```
|
|
35
38
|
|
|
39
|
+
For `type="password"`, do not add a separate eye icon or clear button. The built-in password toggle is automatic; `allowClear` is hidden on password inputs.
|
|
40
|
+
|
|
36
41
|
### Form Integration (rc-field-form)
|
|
37
42
|
|
|
38
43
|
`Form` / `FormItem` / `SchemaForm` wrap `rc-field-form`. `FormItem` auto-clones children to inject `value`/`onChange`. Prefer `SchemaForm` for data-driven forms:
|
|
@@ -45,12 +45,15 @@ interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, '
|
|
|
45
45
|
onChange?: (value: string, event: ChangeEvent<HTMLInputElement>) => void
|
|
46
46
|
allowClear?: boolean | { clearIcon: ReactNode }
|
|
47
47
|
addonAfter?: ReactNode
|
|
48
|
+
inputClassName?: string
|
|
48
49
|
className?: string
|
|
49
50
|
}
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
Note: `onChange` callback receives `(stringValue, event)` — two arguments, not one.
|
|
53
54
|
|
|
55
|
+
Use `type="password"` for password fields. The component shows a built-in visibility toggle, supports controlled and uncontrolled values, and removes the native DOM `value` attribute while preserving the input value property. `allowClear` is intentionally hidden for password inputs. `addonAfter` shares the right-side accessory area with the password toggle.
|
|
56
|
+
|
|
54
57
|
---
|
|
55
58
|
|
|
56
59
|
## InputNumber
|
package/AGENT_README.md
CHANGED
|
@@ -74,9 +74,15 @@ main().catch((error) => {
|
|
|
74
74
|
// config/config.default.ts
|
|
75
75
|
export default {
|
|
76
76
|
port: 9000,
|
|
77
|
+
auth: {
|
|
78
|
+
ATKey: 'Authorization',
|
|
79
|
+
RTKey: 'RT',
|
|
80
|
+
},
|
|
77
81
|
}
|
|
78
82
|
```
|
|
79
83
|
|
|
84
|
+
`auth.ATKey` / `auth.RTKey` 是业务鉴权中间件读取短 token 和刷新 token header 的默认约定;默认分别是 `Authorization` / `RT`。
|
|
85
|
+
|
|
80
86
|
访问内置后台:
|
|
81
87
|
|
|
82
88
|
```text
|
|
@@ -93,6 +99,7 @@ http://localhost:9000/dash?projk=demo
|
|
|
93
99
|
app/controller/**/*.(js|ts) -> app.controller
|
|
94
100
|
app/service/**/*.(js|ts) -> app.service
|
|
95
101
|
app/middleware/**/*.(js|ts) -> app.middlewares
|
|
102
|
+
app/middleware.(js|ts) -> 全局中间件编排,按 frame -> business 顺序执行
|
|
96
103
|
app/router/**/*.(js|ts) -> Koa router
|
|
97
104
|
app/router-guard.(js|ts) -> 兜底路由守卫
|
|
98
105
|
app/router-schema/**/*.(js|ts) -> app.routerSchema
|
|
@@ -102,6 +109,24 @@ config/config.{env}.(js|ts) -> app.config
|
|
|
102
109
|
model/**/*.(js|ts) -> 内置 project service 读取的项目模型配置
|
|
103
110
|
```
|
|
104
111
|
|
|
112
|
+
常用后端类型:
|
|
113
|
+
|
|
114
|
+
| 类型 | 使用场景 |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `Ctx` | Controller、middleware、router handler 的请求上下文,包含 `ctx.params` 和可选 `ctx.reqData`。 |
|
|
117
|
+
| `ControllerFN` | 约束 `app/controller/*.ts` 默认导出的 controller 工厂。 |
|
|
118
|
+
| `ServiceFN` | 约束 `app/service/*.ts` 默认导出的 service 工厂。 |
|
|
119
|
+
| `MiddlewareFN` | 约束 `app/middleware/*.ts` 默认导出的 middleware 工厂。 |
|
|
120
|
+
| `RouterFN` | 约束 `app/router/*.ts` 默认导出的路由注册函数。 |
|
|
121
|
+
|
|
122
|
+
`ctx.reqData` 是 API 请求的统一参数入口:
|
|
123
|
+
|
|
124
|
+
- `ctx.reqData.query` 来自 `ctx.query`。
|
|
125
|
+
- `ctx.reqData.body` 来自 `ctx.request.body`。
|
|
126
|
+
- `ctx.reqData.headers` 来自 `ctx.headers`。
|
|
127
|
+
- `ctx.reqData.data` 按 `query -> body` 合并;字段冲突时 body 覆盖 query。
|
|
128
|
+
- router params 不在 `reqData` 里,继续用 `ctx.params`。
|
|
129
|
+
|
|
105
130
|
Controller 示例:
|
|
106
131
|
|
|
107
132
|
```ts
|
|
@@ -118,15 +143,15 @@ const getProductController = ((app) => {
|
|
|
118
143
|
list = async (ctx: Ctx) => {
|
|
119
144
|
this.success(ctx, {
|
|
120
145
|
data: products,
|
|
121
|
-
page: Number(ctx.
|
|
122
|
-
pageSize: Number(ctx.
|
|
146
|
+
page: Number(ctx.reqData?.data.page || 1),
|
|
147
|
+
pageSize: Number(ctx.reqData?.data.pageSize || 10),
|
|
123
148
|
total: products.length,
|
|
124
149
|
})
|
|
125
150
|
}
|
|
126
151
|
|
|
127
152
|
detail = async (ctx: Ctx) => {
|
|
128
153
|
const item = products.find((product) => {
|
|
129
|
-
return product.product_id === ctx.
|
|
154
|
+
return product.product_id === ctx.reqData?.data.product_id
|
|
130
155
|
})
|
|
131
156
|
|
|
132
157
|
this.success(ctx, item ?? null)
|
|
@@ -140,12 +165,12 @@ export default getProductController
|
|
|
140
165
|
Router 示例:
|
|
141
166
|
|
|
142
167
|
```ts
|
|
143
|
-
import type {
|
|
168
|
+
import type { RouterFN } from '@_tc/template-core'
|
|
144
169
|
|
|
145
|
-
export default (app
|
|
170
|
+
export default ((app, router) => {
|
|
146
171
|
router.get('/api/product/list', app.controller.product.list)
|
|
147
172
|
router.get('/api/product', app.controller.product.detail)
|
|
148
|
-
}
|
|
173
|
+
}) satisfies RouterFN
|
|
149
174
|
```
|
|
150
175
|
|
|
151
176
|
路由挂载顺序:
|
|
@@ -156,12 +181,12 @@ export default (app: KoaApp, router: Router) => {
|
|
|
156
181
|
- 通配、兜底、重定向类路由建议设置较大的 `level`,避免抢先匹配业务 API。
|
|
157
182
|
|
|
158
183
|
```ts
|
|
159
|
-
import type {
|
|
184
|
+
import type { RouterFN } from '@_tc/template-core'
|
|
160
185
|
|
|
161
|
-
export default (app
|
|
186
|
+
export default ((app, router) => {
|
|
162
187
|
router.level = 10
|
|
163
188
|
router.get('/api/fallback/:id', app.controller.fallback.detail)
|
|
164
|
-
}
|
|
189
|
+
}) satisfies RouterFN
|
|
165
190
|
```
|
|
166
191
|
|
|
167
192
|
内置响应约定:`BaseController.success(ctx, data)` 返回 `{ data, code: 0, message: 'ok' }`。
|
|
@@ -360,10 +385,14 @@ import { Button, DataTable, Form, Input, Modal, Select } from '@_tc/template-cor
|
|
|
360
385
|
常用前端能力:
|
|
361
386
|
|
|
362
387
|
- 请求方法:`request`、`get`、`post`、`put`、`patch`、`del`。
|
|
388
|
+
- 请求 header:自动添加 `s_t`、`s_sign`、`projk`;存在短 token 时添加 `Authorization: Bearer ${token}`。
|
|
389
|
+
- 本地 key:短 token 是 `localStorage.auth_token`,项目 key 是 `localStorage.p_J_k`;刷新 token header 默认名是 `RT`。
|
|
390
|
+
- `getAuthToken()` 是同步函数,读取前无需 `await`。
|
|
363
391
|
- Schema 通信:`schemaEventBus`、`eventsInfo`、`merge`。
|
|
364
392
|
- 共享状态:`modeStore`、`schemaStore`、`apiFreezerStore` 及对应 hooks。
|
|
365
393
|
- 多语言:`i18nStore.getState().addResources()`,配置文案可写 `$i18n::...`。
|
|
366
394
|
- 主题:`ThemeSwitch` 会同步根节点 `dark` class 和 `localStorage`。
|
|
395
|
+
- `Input type="password"` 会自动显示密码显隐按钮;`allowClear` 不作用于密码输入。
|
|
367
396
|
|
|
368
397
|
不要直接依赖 `frontend/src/...`、`packages/react/ui/...` 这类包内部路径。
|
|
369
398
|
|
package/README.md
CHANGED
|
@@ -101,7 +101,22 @@ main().catch((error) => {
|
|
|
101
101
|
| `homePage` | `string` | 否 | - | 应用首页路径 |
|
|
102
102
|
| `additionalPublicPaths` | `string | string[]` | 否 | - | 追加静态资源目录和 Nunjucks 模板查找目录,必须传绝对路径 |
|
|
103
103
|
|
|
104
|
-
框架会自动读取 `config/config.default.ts` 中的配置(如 `port`、`env` 等)。
|
|
104
|
+
框架会自动读取 `config/config.default.ts` 中的配置(如 `port`、`env`、`auth` 等)。
|
|
105
|
+
|
|
106
|
+
常用默认配置示例:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// config/config.default.ts
|
|
110
|
+
export default {
|
|
111
|
+
port: 9000,
|
|
112
|
+
auth: {
|
|
113
|
+
ATKey: 'Authorization',
|
|
114
|
+
RTKey: 'RT',
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`auth.ATKey` / `auth.RTKey` 用于约定业务鉴权中间件读取短 token 和刷新 token 的 header 名;默认分别为 `Authorization` 和 `RT`。
|
|
105
120
|
|
|
106
121
|
静态资源挂载和 Nunjucks 模板查找顺序均为:框架 `app/public`、项目 `app/public`、`additionalPublicPaths`。当 `buildFE` 输出到自定义目录时,可以用 `additionalPublicPaths` 把该目录交给 Koa 提供静态资源访问和模板渲染。
|
|
107
122
|
|
|
@@ -165,6 +180,49 @@ http://localhost:9000/dash?projk=demo
|
|
|
165
180
|
| `@_tc/template-core/fe/tailwind_ui.css` | 前端全局样式入口,源码对应 `frontend/src/main.css` |
|
|
166
181
|
| `@_tc/template-core/model` | model 配置类型 |
|
|
167
182
|
|
|
183
|
+
### 后端类型
|
|
184
|
+
|
|
185
|
+
`@_tc/template-core` 会导出常用 Node 侧类型,业务项目写 `app/*` 文件时建议优先复用这些类型。
|
|
186
|
+
|
|
187
|
+
| 类型 | 使用场景 |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `Ctx` | Controller、middleware、router handler 的 Koa 请求上下文类型,包含框架补充的 `ctx.params` 和可选 `ctx.reqData`。 |
|
|
190
|
+
| `ControllerFN` | `app/controller/*.ts` 默认导出工厂函数的约束类型,适合配合 `satisfies ControllerFN` 保留 controller 实例方法类型。 |
|
|
191
|
+
| `ServiceFN` | `app/service/*.ts` 默认导出工厂函数的约束类型,适合配合 `satisfies ServiceFN` 约束 service class 工厂。 |
|
|
192
|
+
| `MiddlewareFN` | `app/middleware/*.ts` 默认导出工厂函数的约束类型,返回标准 Koa middleware。 |
|
|
193
|
+
| `RouterFN` | `app/router/*.ts` 默认导出函数的约束类型,用于接收 `app` 和 `router` 并注册路由。 |
|
|
194
|
+
|
|
195
|
+
示例:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { baseFn, type ControllerFN, type Ctx, type MiddlewareFN, type RouterFN, type ServiceFN } from '@_tc/template-core'
|
|
199
|
+
|
|
200
|
+
export const getDemoController = ((app) => {
|
|
201
|
+
const BaseController = baseFn.baseControllerFn(app)
|
|
202
|
+
|
|
203
|
+
return class DemoController extends BaseController {
|
|
204
|
+
list = async (ctx: Ctx) => {
|
|
205
|
+
this.success(ctx, ctx.reqData?.data ?? {})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}) satisfies ControllerFN
|
|
209
|
+
|
|
210
|
+
export const getDemoService = ((app) => {
|
|
211
|
+
return class DemoService {
|
|
212
|
+
readonly app = app
|
|
213
|
+
}
|
|
214
|
+
}) satisfies ServiceFN
|
|
215
|
+
|
|
216
|
+
export const demoMiddleware = ((app) => async (ctx, next) => {
|
|
217
|
+
app.extends.logger?.info(ctx.path)
|
|
218
|
+
await next()
|
|
219
|
+
}) satisfies MiddlewareFN
|
|
220
|
+
|
|
221
|
+
export const demoRouter = ((app, router) => {
|
|
222
|
+
router.get(`${app.options.apiPrefix}/demo/list`, app.controller.demo.list)
|
|
223
|
+
}) satisfies RouterFN
|
|
224
|
+
```
|
|
225
|
+
|
|
168
226
|
## 样式与 Tailwind V4
|
|
169
227
|
|
|
170
228
|
使用内置前端或 UI 组件时,需要引入包内样式。默认 `@_tc/template-core/fe` 的 `initApp` 会经过源码入口 `frontend/src/main.tsx` 引入 `frontend/src/main.css`;如果项目侧自定义入口、只使用 `fe/rc` 组件,或希望样式入口更明确,建议显式引入:
|
|
@@ -463,6 +521,8 @@ export function Toolbar() {
|
|
|
463
521
|
}
|
|
464
522
|
```
|
|
465
523
|
|
|
524
|
+
`Authorization` 默认对应服务端配置 `config.auth.ATKey`。前端本地 token 存在 `localStorage.auth_token`,项目 key 存在 `localStorage.p_J_k`;刷新 token header 名预留为 `config.auth.RTKey`,默认是 `RT`。
|
|
525
|
+
|
|
466
526
|
**签名机制**:
|
|
467
527
|
- 签名密钥:服务端从 `app.config.signKey` 读取,并在页面渲染时注入为 `window._signKey`
|
|
468
528
|
- 签名算法:`md5(签名密钥_${时间戳})`
|
|
@@ -540,7 +600,7 @@ try {
|
|
|
540
600
|
| 组件 | 用途 | 导入路径 |
|
|
541
601
|
| --- | --- | --- |
|
|
542
602
|
| `Button` | 按钮组件,支持多种样式和尺寸 | `@_tc/template-core/fe/rc` |
|
|
543
|
-
| `Input` |
|
|
603
|
+
| `Input` | 输入框组件,支持清空、右侧附加内容和密码显隐 | `@_tc/template-core/fe/rc` |
|
|
544
604
|
| `InputNumber` | 数字输入框组件 | `@_tc/template-core/fe/rc` |
|
|
545
605
|
| `Textarea` | 多行文本输入框组件 | `@_tc/template-core/fe/rc` |
|
|
546
606
|
| `Select` | 下拉选择组件,支持单选和多选 | `@_tc/template-core/fe/rc` |
|
|
@@ -623,6 +683,7 @@ function MyComponent() {
|
|
|
623
683
|
</Button>
|
|
624
684
|
|
|
625
685
|
<Input placeholder="请输入内容" />
|
|
686
|
+
<Input type="password" placeholder="请输入密码" />
|
|
626
687
|
|
|
627
688
|
<Select
|
|
628
689
|
options={[
|
|
@@ -648,6 +709,7 @@ function MyComponent() {
|
|
|
648
709
|
app/controller/**/*.(js|ts) -> app.controller
|
|
649
710
|
app/service/**/*.(js|ts) -> app.service
|
|
650
711
|
app/middleware/**/*.(js|ts) -> app.middlewares
|
|
712
|
+
app/middleware.(js|ts) -> 全局中间件编排,按框架 -> 项目顺序执行
|
|
651
713
|
app/router/**/*.(js|ts) -> Koa router
|
|
652
714
|
app/router-schema/**/*.(js|ts) -> app.routerSchema
|
|
653
715
|
app/extend/*.(js|ts) -> app.extends
|
|
@@ -662,6 +724,24 @@ model/**/*.(js|ts) -> 项目模型配置
|
|
|
662
724
|
- `app.extends.crypto`:Node 侧加密辅助方法,支持 base64url 编解码、HMAC 签名、签名 payload 和常量时间比较。
|
|
663
725
|
- `app.extends.db`:默认 SQLite 框架数据库,提供 `getDBData/setDBData` 等通用数据方法。
|
|
664
726
|
|
|
727
|
+
API controller 可以通过 `ctx.reqData` 读取统一请求参数:
|
|
728
|
+
|
|
729
|
+
```ts
|
|
730
|
+
const { query, body, headers, data } = ctx.reqData ?? {
|
|
731
|
+
query: {},
|
|
732
|
+
body: undefined,
|
|
733
|
+
headers: {},
|
|
734
|
+
data: {},
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
- `query` 来自 `ctx.query`。
|
|
739
|
+
- `body` 来自 `ctx.request.body`。
|
|
740
|
+
- `headers` 来自 `ctx.headers`。
|
|
741
|
+
- `data` 是便捷合并层,当前按 `query -> body` 顺序合并;字段冲突时 body 覆盖 query。
|
|
742
|
+
|
|
743
|
+
`ctx.reqData` 只在 API 请求中由内置中间件注入,类型上是可选字段。router params 不在 `reqData` 里,仍然从 `ctx.params` 获取。
|
|
744
|
+
|
|
665
745
|
### 数据库扩展
|
|
666
746
|
|
|
667
747
|
默认 `app.extends.db` 使用 SQLite 保存数据,数据库文件位于项目根目录 `.template-core/template-core.sqlite`。可以在配置中调整路径:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var e={frameName:`TemplateCore`,signKey:`sakjdfnkjwjfnfkjkjldljksndf`,apiSignVerify:{whiteList:[]},notFoundRedirectCount:5};module.exports=e;
|
|
1
|
+
var e={frameName:`TemplateCore`,signKey:`sakjdfnkjwjfnfkjkjldljksndf`,apiSignVerify:{whiteList:[]},auth:{ATKey:`Authorization`,RTKey:`RT`},notFoundRedirectCount:5};module.exports=e;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`../../_virtual/_rolldown/runtime.js`),t=require(`./
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`../../_virtual/_rolldown/runtime.js`),t=require(`./paths.js`),n=require(`./env.js`),r=require(`../utils/path.js`),i=require(`../utils/runFileFn.js`);require(`../utils/index.js`);const a=require(`./loader/config.js`),o=require(`./loader/controller.js`),s=require(`./loader/extend.js`),c=require(`./loader/middleware.js`),l=require(`./loader/model.js`),u=require(`./loader/router.js`),d=require(`./loader/router-schema.js`),f=require(`./loader/service.js`);let p=require(`koa`);p=e.__toESM(p,1);var m=l,h=async(e={})=>{let t=new p.default;t.hooks={},t.hooks.initing?.();let{frameBaseDir:i,baseDir:l,...m}=e;t.options=m,t.options.pageBasePage=t.options.pageBasePage??`/`,t.options.apiPrefix=t.options.apiPrefix||`/api`;let h=t.baseDir=l??process.cwd(),_=t.frameBaseDir=i??r.resolve(__dirname,`..`);t.businessAppPath=r.resolve(h,`app`),t.frameAppPath=r.resolve(_,`app`),t.paths=[{type:`frame`,path:t.frameAppPath},{type:`business`,path:t.businessAppPath}],t.envs=n(),console.log(`-- [init] env: ${t.envs.get()} --`),a(t),console.log(`-- [init] loader config done --`),await s(t),console.log(`-- [init] loader extend done --`),d(t),console.log(`-- [init] loader routerSchema done --`),f(t),console.log(`-- [init] loader service done --`),o(t),console.log(`-- [init] loader controller done --`),c(t),console.log(`-- [init] loader middleware done --`),g(t),u(t),console.log(`-- [init] loader router done --`);try{let e=t.config.port||process.env.PORT||`8080`,r=parseInt(e+``,10),i=process.env.IP||`0.0.0.0`;t.listen(r,i),console.log(`Server port: http://localhost:${r}`),console.log(`run env is ${n().get()}`)}catch(e){console.error(e)}return t.hooks.inited?.(),t},g=e=>{[`frame`,`business`].map(n=>({type:n,path:t.getAppPath(e,n)})).forEach(t=>{for(let n of[`ts`,`js`]){let a=`middleware.${n}`,o=r.resolve(t.path,a);try{i.runFileFn(o)?.(e),console.log(`-- [init] loader ${t.type} appMiddleware ${a} done -- (${t.path})`);break}catch(e){let n=e;if(!(n?.code===`MODULE_NOT_FOUND`&&typeof n?.message==`string`&&(n.message.includes(`Cannot find module '${o}'`)||n.message.includes(`Cannot find module "${o}"`))))throw console.log(`-- [error] loader ${t.type} appMiddleware ${a} error -- (${o})`),e}}})};exports.loaderModel=m,exports.start=h;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=require(`../paths.js`),t=require(`../../utils/loadFile.js`),n=require(`../../utils/runFileFn.js`);require(`../../utils/index.js`);var r=r=>{r.middlewares=e.getAppPaths(r).reduce((e,i)=>{let a=t.loadFileFn(i,`
|
|
1
|
+
const e=require(`../paths.js`),t=require(`../../utils/loadFile.js`),n=require(`../../utils/runFileFn.js`);require(`../../utils/index.js`);var r=r=>{r.middlewares=e.getAppPaths(r).reduce((e,i)=>{let a=t.loadFileFn(i,`middlewares`,{loadFileCallback:e=>n.runFileFn(e)(r)});return{...e,...a}},{})};module.exports=r;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import e from "md5";
|
|
2
|
-
//#region app/
|
|
2
|
+
//#region app/middlewares/api-sign-verify.ts
|
|
3
3
|
var t = 1e3 * 60 * 10, n = (n) => async (r, i) => {
|
|
4
4
|
let { path: a, method: o } = r;
|
|
5
5
|
if (a.includes(n.options.apiPrefix) && !n.config?.apiSignVerify?.whiteList?.includes(a)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region app/
|
|
1
|
+
//#region app/middlewares/project-handler.ts
|
|
2
2
|
var e = (e) => async (t, n) => {
|
|
3
3
|
let r = t.headers.projk, { path: i } = t;
|
|
4
4
|
if (i.indexOf(`${e.options.apiPrefix}/proj/`) !== -1 && (!r || ["undefined", "null"].includes(r))) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region app/
|
|
1
|
+
//#region app/middlewares/request-parameter-parsing.ts
|
|
2
2
|
var e = (e) => typeof e == "object" && !!e && !Array.isArray(e), t = ((t) => async (n, r) => {
|
|
3
3
|
if (n.path.includes(`${t.options.apiPrefix}/`)) {
|
|
4
4
|
let { query: t, headers: r } = n, i = n.request.body;
|
|
@@ -1,50 +1,54 @@
|
|
|
1
|
-
import e from "./
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import { getAppPath as e } from "./paths.js";
|
|
2
|
+
import t from "./env.js";
|
|
3
|
+
import { resolve as n } from "../utils/path.js";
|
|
4
|
+
import { runFileFn as r } from "../utils/runFileFn.js";
|
|
4
5
|
import "../utils/index.js";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
6
|
+
import i from "./loader/config.js";
|
|
7
|
+
import a from "./loader/controller.js";
|
|
8
|
+
import o from "./loader/extend.js";
|
|
9
|
+
import s from "./loader/middleware.js";
|
|
10
|
+
import c from "./loader/model.js";
|
|
11
|
+
import l from "./loader/router.js";
|
|
12
|
+
import u from "./loader/router-schema.js";
|
|
13
|
+
import d from "./loader/service.js";
|
|
14
|
+
import f from "koa";
|
|
14
15
|
//#region packages/core/index.ts
|
|
15
|
-
var
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
let { frameBaseDir:
|
|
19
|
-
|
|
20
|
-
let g =
|
|
21
|
-
|
|
16
|
+
var p = c, m = async (e = {}) => {
|
|
17
|
+
let r = new f();
|
|
18
|
+
r.hooks = {}, r.hooks.initing?.();
|
|
19
|
+
let { frameBaseDir: c, baseDir: p, ...m } = e;
|
|
20
|
+
r.options = m, r.options.pageBasePage = r.options.pageBasePage ?? "/", r.options.apiPrefix = r.options.apiPrefix || "/api";
|
|
21
|
+
let g = r.baseDir = p ?? process.cwd(), _ = r.frameBaseDir = c ?? n(__dirname, "..");
|
|
22
|
+
r.businessAppPath = n(g, "app"), r.frameAppPath = n(_, "app"), r.paths = [{
|
|
22
23
|
type: "frame",
|
|
23
|
-
path:
|
|
24
|
+
path: r.frameAppPath
|
|
24
25
|
}, {
|
|
25
26
|
type: "business",
|
|
26
|
-
path:
|
|
27
|
-
}],
|
|
27
|
+
path: r.businessAppPath
|
|
28
|
+
}], r.envs = t(), console.log(`-- [init] env: ${r.envs.get()} --`), i(r), console.log("-- [init] loader config done --"), await o(r), console.log("-- [init] loader extend done --"), u(r), console.log("-- [init] loader routerSchema done --"), d(r), console.log("-- [init] loader service done --"), a(r), console.log("-- [init] loader controller done --"), s(r), console.log("-- [init] loader middleware done --"), h(r), l(r), console.log("-- [init] loader router done --");
|
|
28
29
|
try {
|
|
29
|
-
let
|
|
30
|
-
|
|
30
|
+
let e = r.config.port || process.env.PORT || "8080", n = parseInt(e + "", 10), i = process.env.IP || "0.0.0.0";
|
|
31
|
+
r.listen(n, i), console.log(`Server port: http://localhost:${n}`), console.log(`run env is ${t().get()}`);
|
|
31
32
|
} catch (e) {
|
|
32
33
|
console.error(e);
|
|
33
34
|
}
|
|
34
|
-
return
|
|
35
|
-
},
|
|
36
|
-
|
|
35
|
+
return r.hooks.inited?.(), r;
|
|
36
|
+
}, h = (t) => {
|
|
37
|
+
["frame", "business"].map((n) => ({
|
|
38
|
+
type: n,
|
|
39
|
+
path: e(t, n)
|
|
40
|
+
})).forEach((e) => {
|
|
37
41
|
for (let i of ["ts", "js"]) {
|
|
38
|
-
let a = `middleware.${i}`, o =
|
|
42
|
+
let a = `middleware.${i}`, o = n(e.path, a);
|
|
39
43
|
try {
|
|
40
|
-
|
|
44
|
+
r(o)?.(t), console.log(`-- [init] loader ${e.type} appMiddleware ${a} done -- (${e.path})`);
|
|
41
45
|
break;
|
|
42
|
-
} catch (
|
|
43
|
-
let
|
|
44
|
-
if (!(
|
|
46
|
+
} catch (t) {
|
|
47
|
+
let n = t;
|
|
48
|
+
if (!(n?.code === "MODULE_NOT_FOUND" && typeof n?.message == "string" && (n.message.includes(`Cannot find module '${o}'`) || n.message.includes(`Cannot find module "${o}"`)))) throw console.log(`-- [error] loader ${e.type} appMiddleware ${a} error -- (${o})`), t;
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
});
|
|
48
52
|
};
|
|
49
53
|
//#endregion
|
|
50
|
-
export {
|
|
54
|
+
export { p as loaderModel, m as start };
|
|
@@ -5,7 +5,7 @@ import "../../utils/index.js";
|
|
|
5
5
|
//#region packages/core/loader/middleware.ts
|
|
6
6
|
var r = (r) => {
|
|
7
7
|
r.middlewares = e(r).reduce((e, i) => {
|
|
8
|
-
let a = t(i, "
|
|
8
|
+
let a = t(i, "middlewares", { loadFileCallback: (e) => n(e)(r) });
|
|
9
9
|
return {
|
|
10
10
|
...e,
|
|
11
11
|
...a
|
|
@@ -96,7 +96,7 @@ var removeBaseurlFromURL = (config) => {
|
|
|
96
96
|
};
|
|
97
97
|
api.interceptors.request.use(async (config) => {
|
|
98
98
|
removeBaseurlFromURL(config);
|
|
99
|
-
const token =
|
|
99
|
+
const token = getAuthToken();
|
|
100
100
|
const aauto = getApiAuth();
|
|
101
101
|
const time = (/* @__PURE__ */ new Date()).getTime();
|
|
102
102
|
config.headers = {
|
|
@@ -1,24 +1,55 @@
|
|
|
1
1
|
import { cn } from "../../lib/utils.js";
|
|
2
2
|
import { useInputController } from "../hooks/useInputController.js";
|
|
3
|
-
import { forwardRef, useCallback, useMemo } from "react";
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import { X } from "lucide-react";
|
|
5
|
+
import { Eye, EyeOff, X } from "lucide-react";
|
|
6
6
|
//#region packages/react/ui/components/Input/Input.tsx
|
|
7
7
|
var Input = forwardRef(({ className, inputClassName, type, value: controlledValue, onChange, defaultValue, allowClear, disabled, addonAfter, ...props }, ref) => {
|
|
8
|
+
const isFileInput = type === "file";
|
|
9
|
+
const isPasswordInput = type === "password";
|
|
8
10
|
const { setInternalValue, setIsFocused, setRefs, controllerRef, isControlledRef, isFocused, value, hasValue } = useInputController({
|
|
9
11
|
defaultValue,
|
|
10
12
|
controlledValue,
|
|
11
13
|
ref
|
|
12
14
|
});
|
|
13
|
-
const
|
|
15
|
+
const [innerType, setInnerType] = useState(type);
|
|
16
|
+
const isMountedRef = useRef(false);
|
|
17
|
+
const currentType = isPasswordInput ? innerType ?? "password" : type;
|
|
18
|
+
const isPasswordVisible = currentType === "text";
|
|
19
|
+
const removePasswordValueAttribute = useCallback(() => {
|
|
20
|
+
if (!isPasswordInput) return;
|
|
21
|
+
Promise.resolve().then(() => {
|
|
22
|
+
if (!isMountedRef.current) return;
|
|
23
|
+
const input = controllerRef.current;
|
|
24
|
+
if (input?.getAttribute("type") === "password" && input.hasAttribute("value")) input.removeAttribute("value");
|
|
25
|
+
});
|
|
26
|
+
}, [controllerRef, isPasswordInput]);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
isMountedRef.current = true;
|
|
29
|
+
return () => {
|
|
30
|
+
isMountedRef.current = false;
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setInnerType(type);
|
|
35
|
+
}, [type]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
removePasswordValueAttribute();
|
|
38
|
+
}, [
|
|
39
|
+
currentType,
|
|
40
|
+
removePasswordValueAttribute,
|
|
41
|
+
value
|
|
42
|
+
]);
|
|
14
43
|
const handleChange = useCallback((e) => {
|
|
15
44
|
const newValue = e.target.value;
|
|
45
|
+
removePasswordValueAttribute();
|
|
16
46
|
if (!isFileInput && !isControlledRef.current) setInternalValue(newValue);
|
|
17
47
|
onChange?.(newValue, e);
|
|
18
48
|
}, [
|
|
19
49
|
isFileInput,
|
|
20
50
|
isControlledRef,
|
|
21
51
|
onChange,
|
|
52
|
+
removePasswordValueAttribute,
|
|
22
53
|
setInternalValue
|
|
23
54
|
]);
|
|
24
55
|
const handleClear = useCallback((e) => {
|
|
@@ -36,6 +67,12 @@ var Input = forwardRef(({ className, inputClassName, type, value: controlledValu
|
|
|
36
67
|
onChange,
|
|
37
68
|
setInternalValue
|
|
38
69
|
]);
|
|
70
|
+
const handleTogglePassword = useCallback((e) => {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
if (isPasswordVisible) removePasswordValueAttribute();
|
|
74
|
+
setInnerType((current) => current === "password" ? "text" : "password");
|
|
75
|
+
}, [isPasswordVisible, removePasswordValueAttribute]);
|
|
39
76
|
const clearIcon = useMemo(() => {
|
|
40
77
|
if (allowClear) if (typeof allowClear === "boolean") return /* @__PURE__ */ jsx("span", {
|
|
41
78
|
className: cn("w-5 h-5 rounded-full", "bg-muted hover:bg-muted/80", "text-foreground", "transition-colors", "flex items-center justify-center", "focus:outline-none"),
|
|
@@ -43,44 +80,61 @@ var Input = forwardRef(({ className, inputClassName, type, value: controlledValu
|
|
|
43
80
|
});
|
|
44
81
|
else return allowClear.clearIcon;
|
|
45
82
|
}, [allowClear]);
|
|
46
|
-
const showClearIcon = hasValue && clearIcon && !disabled;
|
|
83
|
+
const showClearIcon = !isPasswordInput && hasValue && clearIcon && !disabled;
|
|
84
|
+
const showPasswordToggle = isPasswordInput && !disabled;
|
|
85
|
+
const rightAccessoryCount = Number(Boolean(showClearIcon)) + Number(showPasswordToggle) + Number(Boolean(addonAfter));
|
|
86
|
+
const hasRightAccessories = rightAccessoryCount > 0;
|
|
47
87
|
return /* @__PURE__ */ jsxs("div", {
|
|
48
88
|
className: cn("tc-ui-input relative w-full", className),
|
|
49
|
-
children: [
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
90
|
+
type: currentType,
|
|
91
|
+
ref: setRefs,
|
|
92
|
+
...!isFileInput && { value },
|
|
93
|
+
onChange: handleChange,
|
|
94
|
+
className: cn("flex h-10 w-full rounded-lg px-3 py-0 text-sm", "border transition-colors duration-200", "placeholder:text-muted-foreground", "focus:outline-none", rightAccessoryCount === 1 && "pr-10", rightAccessoryCount === 2 && "pr-20", rightAccessoryCount >= 3 && "pr-28", disabled ? "cursor-not-allowed bg-muted border-border text-muted-foreground" : "bg-background", !disabled && !isFocused && "border-border", !disabled && isFocused && "border-theme ring-2 ring-theme/25", inputClassName),
|
|
95
|
+
onFocus: (e) => {
|
|
96
|
+
if (!disabled) {
|
|
97
|
+
removePasswordValueAttribute();
|
|
98
|
+
setIsFocused(true);
|
|
99
|
+
props.onFocus?.(e);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
onBlur: (e) => {
|
|
103
|
+
if (!disabled) {
|
|
104
|
+
removePasswordValueAttribute();
|
|
105
|
+
setIsFocused(false);
|
|
106
|
+
props.onBlur?.(e);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
disabled,
|
|
110
|
+
...props
|
|
111
|
+
}), hasRightAccessories && /* @__PURE__ */ jsxs("div", {
|
|
112
|
+
className: "absolute right-1.5 top-1/2 flex -translate-y-1/2 items-center gap-1",
|
|
113
|
+
children: [
|
|
114
|
+
showClearIcon && /* @__PURE__ */ jsx("button", {
|
|
115
|
+
type: "button",
|
|
116
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
117
|
+
onClick: handleClear,
|
|
118
|
+
"aria-label": "Clear input",
|
|
119
|
+
title: "Clear input",
|
|
120
|
+
className: "flex h-7 w-7 items-center justify-center rounded-md transition-colors hover:bg-foreground/8",
|
|
121
|
+
children: clearIcon
|
|
122
|
+
}),
|
|
123
|
+
showPasswordToggle && /* @__PURE__ */ jsx("button", {
|
|
124
|
+
type: "button",
|
|
125
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
126
|
+
onClick: handleTogglePassword,
|
|
127
|
+
"aria-label": isPasswordVisible ? "Hide password" : "Show password",
|
|
128
|
+
title: isPasswordVisible ? "Hide password" : "Show password",
|
|
129
|
+
className: "flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-foreground/8 hover:text-foreground",
|
|
130
|
+
children: isPasswordVisible ? /* @__PURE__ */ jsx(EyeOff, { size: 16 }) : /* @__PURE__ */ jsx(Eye, { size: 16 })
|
|
131
|
+
}),
|
|
132
|
+
addonAfter && /* @__PURE__ */ jsx("div", {
|
|
133
|
+
className: "flex h-7 items-center text-muted-foreground",
|
|
134
|
+
children: addonAfter
|
|
135
|
+
})
|
|
136
|
+
]
|
|
137
|
+
})]
|
|
84
138
|
});
|
|
85
139
|
});
|
|
86
140
|
Input.displayName = "Input";
|
|
@@ -209,14 +209,40 @@ function InputDemo() {
|
|
|
209
209
|
return /* @__PURE__ */ jsxs(Section, {
|
|
210
210
|
title: "Input",
|
|
211
211
|
children: [
|
|
212
|
-
/* @__PURE__ */ jsx(Input, {
|
|
212
|
+
/* @__PURE__ */ jsx(Input, {
|
|
213
|
+
placeholder: "普通输入框",
|
|
214
|
+
className: "w-48"
|
|
215
|
+
}),
|
|
216
|
+
/* @__PURE__ */ jsx(Input, {
|
|
217
|
+
allowClear: true,
|
|
218
|
+
defaultValue: "可清空内容",
|
|
219
|
+
placeholder: "清空按钮",
|
|
220
|
+
className: "w-48"
|
|
221
|
+
}),
|
|
222
|
+
/* @__PURE__ */ jsx(Input, {
|
|
223
|
+
type: "password",
|
|
224
|
+
placeholder: "密码输入框",
|
|
225
|
+
className: "w-48"
|
|
226
|
+
}),
|
|
227
|
+
/* @__PURE__ */ jsx(Input, {
|
|
228
|
+
type: "password",
|
|
229
|
+
defaultValue: "template-core",
|
|
230
|
+
placeholder: "密码默认值",
|
|
231
|
+
className: "w-48"
|
|
232
|
+
}),
|
|
213
233
|
/* @__PURE__ */ jsx(Input, {
|
|
214
234
|
type: "password",
|
|
215
|
-
placeholder: "
|
|
235
|
+
placeholder: "密码 + addon",
|
|
236
|
+
addonAfter: /* @__PURE__ */ jsx("span", {
|
|
237
|
+
className: "text-xs text-muted-foreground",
|
|
238
|
+
children: "PWD"
|
|
239
|
+
}),
|
|
240
|
+
className: "w-56"
|
|
216
241
|
}),
|
|
217
242
|
/* @__PURE__ */ jsx(Input, {
|
|
218
243
|
disabled: true,
|
|
219
|
-
placeholder: "禁用状态"
|
|
244
|
+
placeholder: "禁用状态",
|
|
245
|
+
className: "w-48"
|
|
220
246
|
})
|
|
221
247
|
]
|
|
222
248
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@_tc/template-core",
|
|
3
|
-
"version": "0.0.1-bate.
|
|
3
|
+
"version": "0.0.1-bate.59",
|
|
4
4
|
"description": "A full-stack TypeScript admin framework powered by Koa, React, and Vite - monorepo root",
|
|
5
5
|
"types": "./types/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -64,7 +64,12 @@
|
|
|
64
64
|
"tailwindcss": "4.2.4"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@types/
|
|
67
|
+
"@types/json-schema": "7.0.15",
|
|
68
|
+
"@types/md5": "2.3.6",
|
|
69
|
+
"@types/node": "20.19.39",
|
|
70
|
+
"@types/nunjucks": "3.2.6",
|
|
71
|
+
"@types/koa-router": "7.4.9",
|
|
72
|
+
"@types/koa": "2.15.0",
|
|
68
73
|
"@vitejs/plugin-react": "6.0.1",
|
|
69
74
|
"clsx": "^2.1.0",
|
|
70
75
|
"date-fns": "^3.6.0",
|
|
@@ -40,6 +40,32 @@ export interface DefaultAppConfig {
|
|
|
40
40
|
viewsPath?: string;
|
|
41
41
|
nunjucks?: import('nunjucks').ConfigureOptions;
|
|
42
42
|
notFoundRedirectCount?: number;
|
|
43
|
+
/**
|
|
44
|
+
* 登陆验证相关
|
|
45
|
+
*/
|
|
46
|
+
auth?: {
|
|
47
|
+
/**
|
|
48
|
+
* 短token 获取的key
|
|
49
|
+
* @default 'Authorization'
|
|
50
|
+
*/
|
|
51
|
+
ATKey?: string;
|
|
52
|
+
/**
|
|
53
|
+
* 长token 获取的key
|
|
54
|
+
* @default 'RT'
|
|
55
|
+
*/
|
|
56
|
+
RTKey?: string;
|
|
57
|
+
};
|
|
43
58
|
}
|
|
44
|
-
declare const _default:
|
|
59
|
+
declare const _default: {
|
|
60
|
+
frameName: string;
|
|
61
|
+
signKey: string;
|
|
62
|
+
apiSignVerify: {
|
|
63
|
+
whiteList: never[];
|
|
64
|
+
};
|
|
65
|
+
auth: {
|
|
66
|
+
ATKey: string;
|
|
67
|
+
RTKey: string;
|
|
68
|
+
};
|
|
69
|
+
notFoundRedirectCount: number;
|
|
70
|
+
};
|
|
45
71
|
export default _default;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|