@_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.
Files changed (33) hide show
  1. package/.skills/tc-component-usage-skills/SKILL.md +5 -0
  2. package/.skills/tc-component-usage-skills/reference/component-api.md +3 -0
  3. package/.skills/tc-generator/reference/example.md +4 -0
  4. package/.skills/tc-generator/reference/project-template/config/config.default.js +4 -0
  5. package/AGENT_README.md +38 -9
  6. package/README.md +82 -2
  7. package/cjs/config/config.default.js +1 -1
  8. package/cjs/packages/core/index.js +1 -1
  9. package/cjs/packages/core/loader/middleware.js +1 -1
  10. package/esm/app/{middleware → middlewares}/api-params-verify.js +1 -1
  11. package/esm/app/{middleware → middlewares}/api-sign-verify.js +1 -1
  12. package/esm/app/{middleware → middlewares}/error-handle.js +1 -1
  13. package/esm/app/{middleware → middlewares}/project-handler.js +1 -1
  14. package/esm/app/{middleware → middlewares}/request-parameter-parsing.js +1 -1
  15. package/esm/config/config.default.js +4 -0
  16. package/esm/packages/core/index.js +37 -33
  17. package/esm/packages/core/loader/middleware.js +1 -1
  18. package/fe/frontend/src/common/request.js +1 -1
  19. package/fe/packages/react/ui/components/Input/Input.js +93 -39
  20. package/fe/packages/react/ui/components/testPage/index.js +29 -3
  21. package/package.json +7 -2
  22. package/types/config/config.default.d.ts +27 -1
  23. package/types/packages/core/loader/config.d.ts +1 -1
  24. /package/cjs/app/{middleware → middlewares}/api-params-verify.js +0 -0
  25. /package/cjs/app/{middleware → middlewares}/api-sign-verify.js +0 -0
  26. /package/cjs/app/{middleware → middlewares}/error-handle.js +0 -0
  27. /package/cjs/app/{middleware → middlewares}/project-handler.js +0 -0
  28. /package/cjs/app/{middleware → middlewares}/request-parameter-parsing.js +0 -0
  29. /package/types/app/{middleware → middlewares}/api-params-verify.d.ts +0 -0
  30. /package/types/app/{middleware → middlewares}/api-sign-verify.d.ts +0 -0
  31. /package/types/app/{middleware → middlewares}/error-handle.d.ts +0 -0
  32. /package/types/app/{middleware → middlewares}/project-handler.d.ts +0 -0
  33. /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
@@ -48,6 +48,10 @@ main().catch((error) => {
48
48
  ```js
49
49
  module.exports = {
50
50
  port: 9000,
51
+ auth: {
52
+ ATKey: "Authorization",
53
+ RTKey: "RT",
54
+ },
51
55
  };
52
56
  ```
53
57
 
@@ -1,5 +1,9 @@
1
1
  module.exports = {
2
2
  port: 9000,
3
+ auth: {
4
+ ATKey: "Authorization",
5
+ RTKey: "RT",
6
+ },
3
7
  // Optional: override the default `.template-core/template-core.sqlite` path.
4
8
  // db: {
5
9
  // path: "data/app.sqlite",
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.request.query.page || 1),
122
- pageSize: Number(ctx.request.query.pageSize || 10),
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.request.query.product_id
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 { KoaApp, Router } from '@_tc/template-core'
168
+ import type { RouterFN } from '@_tc/template-core'
144
169
 
145
- export default (app: KoaApp, router: Router) => {
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 { KoaApp, Router } from '@_tc/template-core'
184
+ import type { RouterFN } from '@_tc/template-core'
160
185
 
161
- export default (app: KoaApp, router: Router) => {
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` | 输入框组件 | `@_tc/template-core/fe/rc` |
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(`./env.js`),n=require(`../utils/path.js`),r=require(`../utils/runFileFn.js`);require(`../utils/index.js`);const i=require(`./loader/config.js`),a=require(`./loader/controller.js`),o=require(`./loader/extend.js`),s=require(`./loader/middleware.js`),c=require(`./loader/model.js`),l=require(`./loader/router.js`),u=require(`./loader/router-schema.js`),d=require(`./loader/service.js`);let f=require(`koa`);f=e.__toESM(f,1);var p=c,m=async(e={})=>{let r=new f.default;r.hooks={},r.hooks.initing?.();let{frameBaseDir:c,baseDir:p,...m}=e;r.options=m,r.options.pageBasePage=r.options.pageBasePage??`/`,r.options.apiPrefix=r.options.apiPrefix||`/api`;let g=r.baseDir=p??process.cwd(),_=r.frameBaseDir=c??n.resolve(__dirname,`..`);r.businessAppPath=n.resolve(g,`app`),r.frameAppPath=n.resolve(_,`app`),r.paths=[{type:`frame`,path:r.frameAppPath},{type:`business`,path:r.businessAppPath}],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 --`);try{let e=r.config.port||process.env.PORT||`8080`,n=parseInt(e+``,10),i=process.env.IP||`0.0.0.0`;r.listen(n,i),console.log(`Server port: http://localhost:${n}`),console.log(`run env is ${t().get()}`)}catch(e){console.error(e)}return r.hooks.inited?.(),r},h=e=>{e.paths.forEach(t=>{for(let i of[`ts`,`js`]){let a=`middleware.${i}`,o=n.resolve(t.path,a);try{r.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=p,exports.start=m;
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,`middleware`,{loadFileCallback:e=>n.runFileFn(e)(r)});return{...e,...a}},{})};module.exports=r;
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 "ajv";
2
- //#region app/middleware/api-params-verify.ts
2
+ //#region app/middlewares/api-params-verify.ts
3
3
  var t = new e(), n = "http://json-schema.org/draft-07/schema#", r = (e, r, i) => {
4
4
  if (r[e] && i[e]) {
5
5
  i[e].$schema = n;
@@ -1,5 +1,5 @@
1
1
  import e from "md5";
2
- //#region app/middleware/api-sign-verify.ts
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/middleware/error-handle.ts
1
+ //#region app/middlewares/error-handle.ts
2
2
  var e = {}, t = (t) => async (n, r) => {
3
3
  try {
4
4
  await r();
@@ -1,4 +1,4 @@
1
- //#region app/middleware/project-handler.ts
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/middleware/request-parameter-parsing.ts
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;
@@ -3,6 +3,10 @@ var e = {
3
3
  frameName: "TemplateCore",
4
4
  signKey: "sakjdfnkjwjfnfkjkjldljksndf",
5
5
  apiSignVerify: { whiteList: [] },
6
+ auth: {
7
+ ATKey: "Authorization",
8
+ RTKey: "RT"
9
+ },
6
10
  notFoundRedirectCount: 5
7
11
  };
8
12
  //#endregion
@@ -1,50 +1,54 @@
1
- import e from "./env.js";
2
- import { resolve as t } from "../utils/path.js";
3
- import { runFileFn as n } from "../utils/runFileFn.js";
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 r from "./loader/config.js";
6
- import i from "./loader/controller.js";
7
- import a from "./loader/extend.js";
8
- import o from "./loader/middleware.js";
9
- import s from "./loader/model.js";
10
- import c from "./loader/router.js";
11
- import l from "./loader/router-schema.js";
12
- import u from "./loader/service.js";
13
- import d from "koa";
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 f = s, p = async (n = {}) => {
16
- let s = new d();
17
- s.hooks = {}, s.hooks.initing?.();
18
- let { frameBaseDir: f, baseDir: p, ...h } = n;
19
- s.options = h, s.options.pageBasePage = s.options.pageBasePage ?? "/", s.options.apiPrefix = s.options.apiPrefix || "/api";
20
- let g = s.baseDir = p ?? process.cwd(), _ = s.frameBaseDir = f ?? t(__dirname, "..");
21
- s.businessAppPath = t(g, "app"), s.frameAppPath = t(_, "app"), s.paths = [{
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: s.frameAppPath
24
+ path: r.frameAppPath
24
25
  }, {
25
26
  type: "business",
26
- path: s.businessAppPath
27
- }], s.envs = e(), console.log(`-- [init] env: ${s.envs.get()} --`), r(s), console.log("-- [init] loader config done --"), await a(s), console.log("-- [init] loader extend done --"), l(s), console.log("-- [init] loader routerSchema done --"), u(s), console.log("-- [init] loader service done --"), i(s), console.log("-- [init] loader controller done --"), o(s), console.log("-- [init] loader middleware done --"), m(s), c(s), console.log("-- [init] loader router done --");
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 t = s.config.port || process.env.PORT || "8080", n = parseInt(t + "", 10), r = process.env.IP || "0.0.0.0";
30
- s.listen(n, r), console.log(`Server port: http://localhost:${n}`), console.log(`run env is ${e().get()}`);
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 s.hooks.inited?.(), s;
35
- }, m = (e) => {
36
- e.paths.forEach((r) => {
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 = t(r.path, a);
42
+ let a = `middleware.${i}`, o = n(e.path, a);
39
43
  try {
40
- n(o)?.(e), console.log(`-- [init] loader ${r.type} appMiddleware ${a} done -- (${r.path})`);
44
+ r(o)?.(t), console.log(`-- [init] loader ${e.type} appMiddleware ${a} done -- (${e.path})`);
41
45
  break;
42
- } catch (e) {
43
- let t = e;
44
- if (!(t?.code === "MODULE_NOT_FOUND" && typeof t?.message == "string" && (t.message.includes(`Cannot find module '${o}'`) || t.message.includes(`Cannot find module "${o}"`)))) throw console.log(`-- [error] loader ${r.type} appMiddleware ${a} error -- (${o})`), e;
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 { f as loaderModel, p as start };
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, "middleware", { loadFileCallback: (e) => n(e)(r) });
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 = await getAuthToken();
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 isFileInput = type === "file";
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
- /* @__PURE__ */ jsx("input", {
51
- type,
52
- ref: setRefs,
53
- ...!isFileInput && { value },
54
- onChange: handleChange,
55
- 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", showClearIcon && "pr-10", addonAfter && "pr-12", 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),
56
- onFocus: (e) => {
57
- if (!disabled) {
58
- setIsFocused(true);
59
- props.onFocus?.(e);
60
- }
61
- },
62
- onBlur: (e) => {
63
- if (!disabled) {
64
- setIsFocused(false);
65
- props.onBlur?.(e);
66
- }
67
- },
68
- disabled,
69
- ...props
70
- }),
71
- showClearIcon && /* @__PURE__ */ jsx("button", {
72
- type: "button",
73
- onClick: handleClear,
74
- "aria-label": "Clear input",
75
- title: "Clear input",
76
- className: cn("absolute right-3 top-1/2 -translate-y-1/2"),
77
- children: clearIcon
78
- }),
79
- addonAfter && /* @__PURE__ */ jsx("div", {
80
- className: "absolute right-2 top-1/2 -translate-y-1/2",
81
- children: addonAfter
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, { placeholder: "普通输入框" }),
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.57",
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/node": ">=18.0.0",
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: DefaultAppConfig;
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;
@@ -2,7 +2,7 @@ import type { KoaApp } from "../types";
2
2
  /**
3
3
  * config loader 只处理一级
4
4
  *
5
- * @param app 加载 xx/config/ 下的文件
5
+ * @param app 加载 xx/config/ 下的文件 也就是根目录下的
6
6
  * config/config.default.(js|ts)
7
7
  */
8
8
  declare const _default: (app: KoaApp) => void;