@yarch/create-admin 0.1.0
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/README.md +42 -0
- package/bin/create-admin.mjs +245 -0
- package/package.json +20 -0
- package/templates/admin-antd/README.md +29 -0
- package/templates/admin-antd/archetype.json +12 -0
- package/templates/admin-antd/index.html +11 -0
- package/templates/admin-antd/package.json +26 -0
- package/templates/admin-antd/src/app/router.tsx +28 -0
- package/templates/admin-antd/src/components/page-container.tsx +10 -0
- package/templates/admin-antd/src/features/products/api.ts +13 -0
- package/templates/admin-antd/src/features/products/hooks/use-products.ts +19 -0
- package/templates/admin-antd/src/layouts/basic-layout.tsx +27 -0
- package/templates/admin-antd/src/main.tsx +12 -0
- package/templates/admin-antd/src/pages/dashboard.tsx +8 -0
- package/templates/admin-antd/src/pages/login.tsx +16 -0
- package/templates/admin-antd/src/pages/products.tsx +19 -0
- package/templates/admin-antd/src/stores/auth.ts +7 -0
- package/templates/admin-antd/src/types/index.ts +7 -0
- package/templates/admin-antd/src/ui/theme.ts +3 -0
- package/templates/admin-antd/tsconfig.json +16 -0
- package/templates/admin-antd/vite.config.ts +16 -0
- package/templates/admin-arco/README.md +29 -0
- package/templates/admin-arco/archetype.json +12 -0
- package/templates/admin-arco/index.html +11 -0
- package/templates/admin-arco/package.json +26 -0
- package/templates/admin-arco/src/app/router.tsx +28 -0
- package/templates/admin-arco/src/components/page-container.tsx +10 -0
- package/templates/admin-arco/src/features/products/api.ts +13 -0
- package/templates/admin-arco/src/features/products/hooks/use-products.ts +19 -0
- package/templates/admin-arco/src/layouts/basic-layout.tsx +26 -0
- package/templates/admin-arco/src/main.tsx +12 -0
- package/templates/admin-arco/src/pages/dashboard.tsx +8 -0
- package/templates/admin-arco/src/pages/login.tsx +16 -0
- package/templates/admin-arco/src/pages/products.tsx +19 -0
- package/templates/admin-arco/src/stores/auth.ts +7 -0
- package/templates/admin-arco/src/types/index.ts +7 -0
- package/templates/admin-arco/src/ui/theme.ts +3 -0
- package/templates/admin-arco/tsconfig.json +16 -0
- package/templates/admin-arco/vite.config.ts +16 -0
- package/templates/admin-semi/README.md +29 -0
- package/templates/admin-semi/archetype.json +12 -0
- package/templates/admin-semi/index.html +11 -0
- package/templates/admin-semi/package.json +26 -0
- package/templates/admin-semi/src/app/router.tsx +28 -0
- package/templates/admin-semi/src/components/page-container.tsx +10 -0
- package/templates/admin-semi/src/features/products/api.ts +13 -0
- package/templates/admin-semi/src/features/products/hooks/use-products.ts +19 -0
- package/templates/admin-semi/src/layouts/basic-layout.tsx +29 -0
- package/templates/admin-semi/src/main.tsx +12 -0
- package/templates/admin-semi/src/pages/dashboard.tsx +8 -0
- package/templates/admin-semi/src/pages/login.tsx +16 -0
- package/templates/admin-semi/src/pages/products.tsx +19 -0
- package/templates/admin-semi/src/stores/auth.ts +7 -0
- package/templates/admin-semi/src/types/index.ts +7 -0
- package/templates/admin-semi/src/ui/theme.ts +3 -0
- package/templates/admin-semi/tsconfig.json +16 -0
- package/templates/admin-semi/vite.config.ts +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @yarch/create-admin · yarch web 工程生成器
|
|
2
|
+
|
|
3
|
+
> W6-W9(2026-09-03 拍板):npm 分发 + 交互问答 + maven-archetype 式全量渲染 + 模板资产化。
|
|
4
|
+
> golang 栈 `cmd/yarch-init` 的 web 对偶——模板是**声明式资产**(`{{var}}` 占位 + `archetype.json` 变量声明,不要求自身可安装),工程正确性由 CI「生成后冒烟」保证。
|
|
5
|
+
|
|
6
|
+
## 创建工程(一行命令,零 clone)
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# 标准路径:从 npm registry 拉取脚手架(三包已发版后即可用)——
|
|
10
|
+
npm create @yarch/admin@latest ysaas-console # 交互问答:工程名 → UI 档 → 描述 → 端口 → 代理 → GitLab 分组
|
|
11
|
+
npm create @yarch/admin@latest ysaas-console -- --ui antd
|
|
12
|
+
|
|
13
|
+
# 发版前/开发中(本地,等价 golang `go run ./cmd/yarch-init`):
|
|
14
|
+
node stacks/web/packages/create/bin/create-admin.mjs ysaas-console --deps file
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
工程名即一切标识(W7 标准化):npm 包名 = registry.md 服务名,生成器强制 `^[a-z][a-z0-9-]{1,31}$` + 禁裸通用词(一-1/一-2,与 golang 侧同款校验)。
|
|
18
|
+
|
|
19
|
+
## 问答变量(archetype.json 声明,渲染进目录/文件/内容)
|
|
20
|
+
|
|
21
|
+
| 变量 | 问题 | 默认 |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `packageName` | 工程名(= npm 包名 = registry 服务名) | 必填 |
|
|
24
|
+
| `appName` | 展示名(index.html 标题) | = packageName |
|
|
25
|
+
| `description` | 工程描述 | `<名>:基于 yarch web 脚手架生成的中后台工程` |
|
|
26
|
+
| `port` | vite dev 端口 | 5173 |
|
|
27
|
+
| `proxyTarget` | `/api/v1` 反向代理目标 | `http://localhost:8080` |
|
|
28
|
+
| `yarchContractDep` / `yarchReactDep` | @yarch 底座依赖形态 | `--deps version`(默认,`^0.1.0`,标准形态)· `--deps file`(发版前本地过渡 = golang replace 行对偶) |
|
|
29
|
+
|
|
30
|
+
CI 非交互:`--yes` + 全量 flags;渲染后扫描残留 `{{var}}`,有即报错(防模板与变量集漂移)。
|
|
31
|
+
|
|
32
|
+
## 三档模板(templates/,W1 决策)
|
|
33
|
+
|
|
34
|
+
| 档 | UI 库 |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `admin-semi`(默认) | Semi Design(抖音系) |
|
|
37
|
+
| `admin-antd` | antd(蚂蚁系) |
|
|
38
|
+
| `admin-arco` | Arco Design(字节系) |
|
|
39
|
+
|
|
40
|
+
## 发布(W6)
|
|
41
|
+
|
|
42
|
+
推 tag `stacks/web/vX.Y.Z` 触发 [web-publish.yml](../../../.github/workflows/web-publish.yml):发版前跑契约断言 + 依赖机检,随后按 contract → react → create 顺序发布三包(幂等:registry 已有同版本则跳过;需仓库 Secret `NPM_TOKEN`)——**golang「tag → module proxy」的 npm 对偶**。发布前自检:`pnpm --filter <包名> pack --dry-run`。
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @yarch/create-admin 工程生成器(W6-W9)—— maven archetype / cookiecutter 模式的 web 对偶,
|
|
3
|
+
// 与 golang cmd/yarch-init 同构:
|
|
4
|
+
// 模板资产(templates/admin-{semi,antd,arco},{{var}} 占位,不要求自身可安装)
|
|
5
|
+
// + 交互问答(W7)+ 通用渲染引擎(W8:目录/文件/内容全量替换)+ archetype.json 变量声明(W9)。
|
|
6
|
+
// 工程正确性由「生成后冒烟」保证(CI:生成 → install → tsc → build)。
|
|
7
|
+
//
|
|
8
|
+
// 用法(本仓未发版阶段,本地):
|
|
9
|
+
//
|
|
10
|
+
// node stacks/web/packages/create/bin/create-admin.mjs ysaas-console
|
|
11
|
+
// node stacks/web/packages/create/bin/create-admin.mjs ysaas-console --ui antd --yes
|
|
12
|
+
//
|
|
13
|
+
// @yarch/create-admin 发版后无需 clone 本仓(npm create = npm exec):
|
|
14
|
+
//
|
|
15
|
+
// npm create @yarch/admin@latest ysaas-console -- --ui semi
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync, readdirSync, mkdirSync } from "node:fs";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { dirname, join, resolve } from "node:path";
|
|
20
|
+
import readline from "node:readline/promises";
|
|
21
|
+
|
|
22
|
+
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
23
|
+
const UI_TIERS = {
|
|
24
|
+
semi: "Semi Design(抖音系,默认档 W1)",
|
|
25
|
+
antd: "antd(蚂蚁系)",
|
|
26
|
+
arco: "Arco Design(字节系)",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const servicePattern = /^[a-z][a-z0-9-]{1,31}$/;
|
|
30
|
+
// 禁裸通用词(registry.md 一-1/一-2 口径摘录,与 golang yarch-init 同款;完整表以 contract/registry.md 为准)。
|
|
31
|
+
const genericWords = new Set([
|
|
32
|
+
"api", "app", "service", "server", "backend", "web", "admin",
|
|
33
|
+
"main", "common", "system", "demo", "user", "gateway",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// 渲染时跳过的模板资产(变量声明不进生成物;构建产物与依赖不应存在于资产,防御性跳过)。
|
|
37
|
+
const SKIP_FILES = new Set(["archetype.json"]);
|
|
38
|
+
const SKIP_DIRS = new Set(["node_modules", "dist"]);
|
|
39
|
+
const PLACEHOLDER = /\{\{\s*([a-zA-Z][a-zA-Z0-9]*)\s*\}\}/g;
|
|
40
|
+
|
|
41
|
+
function usage() {
|
|
42
|
+
return [
|
|
43
|
+
"用法:create-admin <工程名|输出目录> [--name <名>] [--ui semi|antd|arco] [--desc <描述>]",
|
|
44
|
+
" [--port <端口>] [--proxy <目标>] [--group <GitLab分组>] [--deps file|version]",
|
|
45
|
+
" [--src <模板根>] [--out <目录>] [--yes]",
|
|
46
|
+
" --yes 非交互:缺省项全走默认值(CI 用)",
|
|
47
|
+
" --deps @yarch 底座依赖形态:version(默认,^0.1.0,需 @yarch 已发 npm)| file(发版前本地过渡)",
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseArgs(argv) {
|
|
52
|
+
const opts = { _: [] };
|
|
53
|
+
for (let i = 0; i < argv.length; i++) {
|
|
54
|
+
const a = argv[i];
|
|
55
|
+
if (a === "--yes" || a === "-y") {
|
|
56
|
+
opts.yes = true;
|
|
57
|
+
} else if (a.startsWith("--")) {
|
|
58
|
+
opts[a.slice(2)] = argv[i + 1];
|
|
59
|
+
i++;
|
|
60
|
+
} else {
|
|
61
|
+
opts._.push(a);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return opts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateName(name) {
|
|
68
|
+
if (!servicePattern.test(name)) {
|
|
69
|
+
return "须过 registry.md 一-1 校验:^[a-z][a-z0-9-]{1,31}$(小写字母数字短横线,字母开头,2~32 字符)";
|
|
70
|
+
}
|
|
71
|
+
if (genericWords.has(name)) {
|
|
72
|
+
return "裸通用词禁止单独成名,须带项目/领域前缀或后缀消歧(如 ysaas-admin)——见 registry.md 一-2";
|
|
73
|
+
}
|
|
74
|
+
if (name.includes('"') || name.includes("\\")) {
|
|
75
|
+
return '不得包含双引号与反斜杠(要写进 package.json)';
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function askText(rl, question, { fallback = "", validate = null, optional = false } = {}) {
|
|
81
|
+
for (;;) {
|
|
82
|
+
const hint = fallback ? `(默认 ${fallback})` : optional ? "(可空)" : "";
|
|
83
|
+
const answer = (await rl.question(`${question}${hint}: `)).trim();
|
|
84
|
+
const value = answer || fallback;
|
|
85
|
+
if (!value && !optional) {
|
|
86
|
+
console.log(" ✗ 必填,请输入");
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (value && validate) {
|
|
90
|
+
const err = validate(value);
|
|
91
|
+
if (err) {
|
|
92
|
+
console.log(` ✗ ${err}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function askSelect(rl, question, options, defaultIndex) {
|
|
101
|
+
console.log(question);
|
|
102
|
+
options.forEach((label, i) => console.log(` ${i + 1}) ${label}${i === defaultIndex ? "(默认)" : ""}`));
|
|
103
|
+
for (;;) {
|
|
104
|
+
const answer = (await rl.question(`选择 [${defaultIndex + 1}]: `)).trim();
|
|
105
|
+
const idx = answer ? Number.parseInt(answer, 10) - 1 : defaultIndex;
|
|
106
|
+
if (Number.isInteger(idx) && idx >= 0 && idx < options.length) return idx;
|
|
107
|
+
console.log(" ✗ 无效选项");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderStr(text, vars) {
|
|
112
|
+
return text.replace(PLACEHOLDER, (_, key) => (key in vars ? vars[key] : `{{${key}}}`));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 通用渲染引擎:遍历模板树,文件名与内容逐个 {{var}} 替换;未命中变量的占位符保留,
|
|
116
|
+
// 由 render 后的残留扫描兜底报错(防模板与变量集漂移)。archetype.json 不进生成物。
|
|
117
|
+
function renderTree(src, dst, vars, leftovers) {
|
|
118
|
+
let count = 0;
|
|
119
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
122
|
+
count += renderTree(join(src, entry.name), join(dst, renderStr(entry.name, vars)), vars, leftovers);
|
|
123
|
+
} else if (entry.isFile()) {
|
|
124
|
+
if (SKIP_FILES.has(entry.name)) continue;
|
|
125
|
+
const target = join(dst, renderStr(entry.name, vars));
|
|
126
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
127
|
+
const rendered = renderStr(readFileSync(join(src, entry.name), "utf8"), vars);
|
|
128
|
+
for (const m of rendered.matchAll(PLACEHOLDER)) {
|
|
129
|
+
leftovers.push(`${target}: {{${m[1]}}}`);
|
|
130
|
+
}
|
|
131
|
+
writeFileSync(target, rendered);
|
|
132
|
+
count++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return count;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fatal(message) {
|
|
139
|
+
console.error(`create-admin: ${message}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function main() {
|
|
144
|
+
const args = parseArgs(process.argv.slice(2));
|
|
145
|
+
const nameArg = args.name || (args._.length > 0 && !args._[0].startsWith("/") ? args._[0] : null);
|
|
146
|
+
|
|
147
|
+
const ui = args.ui ?? "semi";
|
|
148
|
+
if (!(ui in UI_TIERS)) fatal(`--ui 须为 ${Object.keys(UI_TIERS).join("|")},当前:${ui}`);
|
|
149
|
+
|
|
150
|
+
const interactive = !args.yes && process.stdin.isTTY;
|
|
151
|
+
if (!nameArg && !interactive) fatal(`工程名必填(或用 --yes 走交互外模式):\n${usage()}`);
|
|
152
|
+
|
|
153
|
+
const rl = interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
154
|
+
try {
|
|
155
|
+
let name = nameArg;
|
|
156
|
+
let description = args.desc;
|
|
157
|
+
let port = args.port;
|
|
158
|
+
let proxyTarget = args.proxy;
|
|
159
|
+
let group = args.group;
|
|
160
|
+
|
|
161
|
+
if (interactive) {
|
|
162
|
+
console.log("yarch web 工程生成器(@yarch/create-admin)\n");
|
|
163
|
+
if (!name) name = await askText(rl, "工程名(= npm 包名 = registry 服务名)", { validate: validateName });
|
|
164
|
+
else if (validateName(name)) fatal(validateName(name));
|
|
165
|
+
if (!description) {
|
|
166
|
+
description = await askText(rl, "工程描述", {
|
|
167
|
+
fallback: `${name}:基于 yarch web 脚手架生成的中后台工程`,
|
|
168
|
+
validate: (v) => (v.includes('"') || v.includes("\\") ? "不得包含双引号与反斜杠(要写进 package.json)" : null),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (!port) {
|
|
172
|
+
port = await askText(rl, "dev 端口", {
|
|
173
|
+
fallback: "5173",
|
|
174
|
+
validate: (v) => (/^\d{4,5}$/.test(v) && Number(v) >= 1024 && Number(v) <= 65535 ? null : "1024~65535 的数字端口号"),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (!proxyTarget) {
|
|
178
|
+
proxyTarget = await askText(rl, "API 反向代理目标(/api/v1)", {
|
|
179
|
+
fallback: "http://localhost:8080",
|
|
180
|
+
validate: (v) => (/^https?:\/\/.+/.test(v) ? null : "须为 http(s):// 开头"),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (group === undefined) group = await askText(rl, "GitLab 分组(用于登记提示)", { optional: true });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!name) fatal(`工程名缺失。\n${usage()}`);
|
|
187
|
+
const nameErr = validateName(name);
|
|
188
|
+
if (nameErr) fatal(`工程名 ${JSON.stringify(name)} ${nameErr}`);
|
|
189
|
+
description ??= `${name}:基于 yarch web 脚手架生成的中后台工程`;
|
|
190
|
+
port ??= "5173";
|
|
191
|
+
proxyTarget ??= "http://localhost:8080";
|
|
192
|
+
group ??= "";
|
|
193
|
+
if (!/^\d{4,5}$/.test(port) || Number(port) < 1024 || Number(port) > 65535) {
|
|
194
|
+
fatal(`端口 ${port} 无效:须为 1024~65535 的数字`);
|
|
195
|
+
}
|
|
196
|
+
if (!/^https?:\/\/.+/.test(proxyTarget)) {
|
|
197
|
+
fatal(`代理目标 ${proxyTarget} 无效:须为 http(s):// 开头`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// @yarch 底座依赖形态(W8):version = ^0.1.0(标准形态,@yarch/contract、@yarch/react 已发 npm);
|
|
201
|
+
// file: 绝对路径 = golang replace 行对偶(发版前本地过渡,yarch 源码变更后重跑 pnpm install 刷新)。
|
|
202
|
+
const depsMode = args.deps === "file" ? "file" : "version";
|
|
203
|
+
const yarchContractDep =
|
|
204
|
+
depsMode === "version" ? "^0.1.0" : `file:${resolve(PKG_ROOT, "../contract")}`;
|
|
205
|
+
const yarchReactDep = depsMode === "version" ? "^0.1.0" : `file:${resolve(PKG_ROOT, "../react")}`;
|
|
206
|
+
|
|
207
|
+
const vars = { packageName: name, appName: name, description, port, proxyTarget, yarchContractDep, yarchReactDep };
|
|
208
|
+
|
|
209
|
+
const outDir = resolve(args.out || (args._.length > 0 ? args._[args._.length - 1] : name));
|
|
210
|
+
let existing;
|
|
211
|
+
try {
|
|
212
|
+
existing = readdirSync(outDir);
|
|
213
|
+
} catch {
|
|
214
|
+
existing = null; // 不存在,直接生成
|
|
215
|
+
}
|
|
216
|
+
if (existing && existing.length > 0) fatal(`输出目录 ${outDir} 非空`);
|
|
217
|
+
|
|
218
|
+
const templateDir = resolve(args.src || join(PKG_ROOT, "templates", `admin-${ui}`));
|
|
219
|
+
try {
|
|
220
|
+
readFileSync(join(templateDir, "archetype.json"));
|
|
221
|
+
} catch {
|
|
222
|
+
fatal(`模板目录不可达或缺少 archetype.json:${templateDir}(在 yarch 仓 stacks/web 下运行,或用 --src 指定)`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const leftovers = [];
|
|
226
|
+
const n = renderTree(templateDir, outDir, vars, leftovers);
|
|
227
|
+
if (leftovers.length > 0) {
|
|
228
|
+
fatal(`模板存在未声明变量(模板与变量集漂移):\n ${leftovers.join("\n ")}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const tierDesc = JSON.parse(readFileSync(join(templateDir, "archetype.json"), "utf8")).description;
|
|
232
|
+
const groupHint = group ? `(属主建议:${group}/${name})` : "";
|
|
233
|
+
console.log(`✅ 已生成 ${outDir}(${n} 个文件,admin-${ui} 档)—— ${tierDesc}
|
|
234
|
+
|
|
235
|
+
下一步:
|
|
236
|
+
1. cd ${outDir} && pnpm install && pnpm dev # http://localhost:${port}
|
|
237
|
+
2. 服务名已置为 ${JSON.stringify(name)}——去 yarch 仓 contract/registry.md 登记表登记${groupHint}
|
|
238
|
+
3. @yarch 底座为 ${depsMode === "version" ? "^0.1.0 版本依赖(升级 = pnpm update @yarch/contract @yarch/react)" : "file: 本地依赖(发版前过渡;正式发版后改用默认 version 形态重新生成或手动替换)"}
|
|
239
|
+
`);
|
|
240
|
+
} finally {
|
|
241
|
+
if (rl) rl.close();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
main().catch((err) => fatal(err.message));
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yarch/create-admin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "yarch web 工程生成器:交互问答 + archetype 全量渲染(maven-archetype/cookiecutter 模式,golang cmd/yarch-init 的 web 对偶)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-admin": "./bin/create-admin.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"templates",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": { "access": "public" },
|
|
15
|
+
"repository": { "type": "git", "url": "git+https://github.com/yuandonghao/yarch.git", "directory": "stacks/web/packages/create" },
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# {{appName}}
|
|
2
|
+
|
|
3
|
+
> {{description}}
|
|
4
|
+
>
|
|
5
|
+
> 由 `@yarch/create-admin` 从 **admin-antd 档**(Vite + React + antd)生成。
|
|
6
|
+
|
|
7
|
+
## 起跑
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install
|
|
11
|
+
pnpm dev # http://localhost:{{port}}
|
|
12
|
+
pnpm build # tsc --noEmit + vite build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 与后端联调
|
|
16
|
+
|
|
17
|
+
`vite.config.ts` 已把 `/api/v1` 反代到 `{{proxyTarget}}`,改 target 即可指向你的后端。
|
|
18
|
+
接口走 `@yarch/contract` 的信封解包(RestResponse / 错误码 / traceId / 401 跳转)。
|
|
19
|
+
|
|
20
|
+
## 登记与升级
|
|
21
|
+
|
|
22
|
+
1. **服务名登记**:`{{packageName}}` 须去 yarch 仓 `contract/registry.md` 二、服务名登记表登记(PR 即登记);
|
|
23
|
+
2. **@yarch 底座依赖**:若 package.json 中为 `^x.y.z` 版本依赖——升级底座 = `pnpm update @yarch/contract @yarch/react`;若为 `file:…` 本地路径(yarch 发版前的过渡形态)——正式发版后替换为 `^x.y.z` 并重跑 `pnpm install`。
|
|
24
|
+
|
|
25
|
+
## 结构纪律(depcruise 同款口径)
|
|
26
|
+
|
|
27
|
+
- `pages/` 薄入口:只组合 `features/`,不得直调 `features/*/api.ts`;
|
|
28
|
+
- `ui/`、`components/` 不得反向依赖 `features/`、`pages/`;
|
|
29
|
+
- 有 JSX 用 `.tsx`,纯逻辑/类型 `.ts`。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "yarch 中后台工程模板 · antd 档(蚂蚁系):Vite + React 19 + react-router 7 + @yarch 契约底座",
|
|
3
|
+
"variables": [
|
|
4
|
+
{ "name": "packageName", "desc": "工程名(= npm 包名 = registry.md 服务名,一-1 校验)", "required": true, "default": "" },
|
|
5
|
+
{ "name": "appName", "desc": "展示名(index.html 标题)", "required": false, "default": "= packageName" },
|
|
6
|
+
{ "name": "description", "desc": "工程描述(package.json)", "required": false, "default": "" },
|
|
7
|
+
{ "name": "port", "desc": "vite dev 端口", "required": false, "default": "5173" },
|
|
8
|
+
{ "name": "proxyTarget", "desc": "/api/v1 反向代理目标", "required": false, "default": "http://localhost:8080" },
|
|
9
|
+
{ "name": "yarchContractDep", "desc": "@yarch/contract 依赖(发版前 file: 本地路径,发版后 ^x.y.z)", "required": false, "default": "" },
|
|
10
|
+
{ "name": "yarchReactDep", "desc": "@yarch/react 依赖(同上)", "required": false, "default": "" }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{packageName}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "{{description}}",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "tsc --noEmit && vite build"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "^19",
|
|
13
|
+
"react-dom": "^19",
|
|
14
|
+
"react-router-dom": "^7",
|
|
15
|
+
"@yarch/contract": "{{yarchContractDep}}",
|
|
16
|
+
"@yarch/react": "{{yarchReactDep}}",
|
|
17
|
+
"antd": "^5"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/react": "^19",
|
|
21
|
+
"@types/react-dom": "^19",
|
|
22
|
+
"@vitejs/plugin-react": "^4",
|
|
23
|
+
"typescript": "^5.6",
|
|
24
|
+
"vite": "^6"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useNavigate, Routes, Route } from "react-router-dom";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { setupReactNavigator } from "@yarch/react";
|
|
4
|
+
import BasicLayout from "../layouts/basic-layout";
|
|
5
|
+
import Dashboard from "../pages/dashboard";
|
|
6
|
+
import Login from "../pages/login";
|
|
7
|
+
import Products from "../pages/products";
|
|
8
|
+
|
|
9
|
+
function NavigatorSetup() {
|
|
10
|
+
const navigate = useNavigate();
|
|
11
|
+
useEffect(() => { setupReactNavigator(navigate); }, [navigate]);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function App() {
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<NavigatorSetup />
|
|
19
|
+
<Routes>
|
|
20
|
+
<Route path="/login" element={<Login />} />
|
|
21
|
+
<Route path="/" element={<BasicLayout />}>
|
|
22
|
+
<Route index element={<Dashboard />} />
|
|
23
|
+
<Route path="products" element={<Products />} />
|
|
24
|
+
</Route>
|
|
25
|
+
</Routes>
|
|
26
|
+
</>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createClient } from "@yarch/contract";
|
|
2
|
+
import type { PageData } from "@yarch/contract";
|
|
3
|
+
import type { Product } from "../../types";
|
|
4
|
+
import { authStore } from "../../stores/auth";
|
|
5
|
+
|
|
6
|
+
const api = createClient({
|
|
7
|
+
baseUrl: "/api/v1",
|
|
8
|
+
getHeaders: () => ({ Authorization: `Bearer ${authStore.token}` }),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function fetchProducts(page = 1, pageSize = 20): Promise<PageData<Product>> {
|
|
12
|
+
return api.get<PageData<Product>>(`/products?page=${page}&pageSize=${pageSize}`);
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import type { PageData } from "@yarch/contract";
|
|
3
|
+
import type { Product } from "../../../types";
|
|
4
|
+
import { fetchProducts } from "../api";
|
|
5
|
+
|
|
6
|
+
export function useProducts() {
|
|
7
|
+
const [data, setData] = useState<PageData<Product> | null>(null);
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
const [error, setError] = useState<Error | null>(null);
|
|
10
|
+
const [page, setPage] = useState(1);
|
|
11
|
+
|
|
12
|
+
const load = useCallback(async (p: number) => {
|
|
13
|
+
setLoading(true); setError(null);
|
|
14
|
+
try { setData(await fetchProducts(p)); } catch (e) { setError(e as Error); } finally { setLoading(false); }
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
useEffect(() => { load(page); }, [page, load]);
|
|
18
|
+
return { data, loading, error, page, setPage, reload: () => load(page) };
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Layout, Menu, Table, Button } from "antd";
|
|
2
|
+
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
export default function BasicLayout() {
|
|
5
|
+
const navigate = useNavigate();
|
|
6
|
+
const location = useLocation();
|
|
7
|
+
const { Sider, Header, Content } = Layout;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Layout style={{ minHeight: "100vh" }}>
|
|
11
|
+
<Sider>
|
|
12
|
+
<Menu selectedKeys={[location.pathname]}
|
|
13
|
+
items={[
|
|
14
|
+
{ key: "/", label: "首页" },
|
|
15
|
+
{ key: "/products", label: "商品" },
|
|
16
|
+
]}
|
|
17
|
+
onClick={({ key }: { key: string }) => navigate(key)} />
|
|
18
|
+
</Sider>
|
|
19
|
+
<Layout>
|
|
20
|
+
<Header style={{ background: "#fff", padding: "0 24px" }} />
|
|
21
|
+
<Content style={{ padding: 24 }}>
|
|
22
|
+
<Outlet />
|
|
23
|
+
</Content>
|
|
24
|
+
</Layout>
|
|
25
|
+
</Layout>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import { BrowserRouter } from "react-router-dom";
|
|
4
|
+
import App from "./app/router";
|
|
5
|
+
|
|
6
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
7
|
+
<React.StrictMode>
|
|
8
|
+
<BrowserRouter>
|
|
9
|
+
<App />
|
|
10
|
+
</BrowserRouter>
|
|
11
|
+
</React.StrictMode>
|
|
12
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Layout, Menu, Table, Button } from "antd";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import { authStore } from "../stores/auth";
|
|
4
|
+
|
|
5
|
+
export default function Login() {
|
|
6
|
+
const navigate = useNavigate();
|
|
7
|
+
return (
|
|
8
|
+
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100vh" }}>
|
|
9
|
+
<form onSubmit={(e: React.FormEvent) => { e.preventDefault(); authStore.token = "demo"; navigate("/"); }}>
|
|
10
|
+
<h2>登录</h2>
|
|
11
|
+
<p>演示——真实认证走 yarch-auth JWT</p>
|
|
12
|
+
<Button type="primary" htmlType="submit" block>登录</Button>
|
|
13
|
+
</form>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Layout, Menu, Table, Button } from "antd";
|
|
2
|
+
import { useProducts } from "../features/products/hooks/use-products";
|
|
3
|
+
|
|
4
|
+
export default function Products() {
|
|
5
|
+
const { data, loading, page, setPage } = useProducts();
|
|
6
|
+
const columns = [
|
|
7
|
+
{ title: "ID", dataIndex: "id", key: "id" },
|
|
8
|
+
{ title: "名称", dataIndex: "name", key: "name" },
|
|
9
|
+
{ title: "价格(分)", dataIndex: "priceCents", key: "priceCents" },
|
|
10
|
+
{ title: "库存", dataIndex: "stock", key: "stock" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div>
|
|
15
|
+
<h2>商品列表</h2>
|
|
16
|
+
<Table columns={columns} dataSource={data?.list ?? []} rowKey="id" loading={loading} pagination={{ current: page, total: data?.total ?? 0, onChange: setPage }} />
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"noEmit": true
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"src"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import react from "@vitejs/plugin-react";
|
|
2
|
+
import { defineConfig } from "vite";
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
plugins: [react()],
|
|
6
|
+
server: {
|
|
7
|
+
port: {{port}},
|
|
8
|
+
proxy: {
|
|
9
|
+
// 后端 API 反向代理(对接 yarch java/golang 栈的信封接口)
|
|
10
|
+
"/api/v1": {
|
|
11
|
+
target: "{{proxyTarget}}",
|
|
12
|
+
changeOrigin: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# {{appName}}
|
|
2
|
+
|
|
3
|
+
> {{description}}
|
|
4
|
+
>
|
|
5
|
+
> 由 `@yarch/create-admin` 从 **admin-arco 档**(Vite + React + Arco Design)生成。
|
|
6
|
+
|
|
7
|
+
## 起跑
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install
|
|
11
|
+
pnpm dev # http://localhost:{{port}}
|
|
12
|
+
pnpm build # tsc --noEmit + vite build
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 与后端联调
|
|
16
|
+
|
|
17
|
+
`vite.config.ts` 已把 `/api/v1` 反代到 `{{proxyTarget}}`,改 target 即可指向你的后端。
|
|
18
|
+
接口走 `@yarch/contract` 的信封解包(RestResponse / 错误码 / traceId / 401 跳转)。
|
|
19
|
+
|
|
20
|
+
## 登记与升级
|
|
21
|
+
|
|
22
|
+
1. **服务名登记**:`{{packageName}}` 须去 yarch 仓 `contract/registry.md` 二、服务名登记表登记(PR 即登记);
|
|
23
|
+
2. **@yarch 底座依赖**:若 package.json 中为 `^x.y.z` 版本依赖——升级底座 = `pnpm update @yarch/contract @yarch/react`;若为 `file:…` 本地路径(yarch 发版前的过渡形态)——正式发版后替换为 `^x.y.z` 并重跑 `pnpm install`。
|
|
24
|
+
|
|
25
|
+
## 结构纪律(depcruise 同款口径)
|
|
26
|
+
|
|
27
|
+
- `pages/` 薄入口:只组合 `features/`,不得直调 `features/*/api.ts`;
|
|
28
|
+
- `ui/`、`components/` 不得反向依赖 `features/`、`pages/`;
|
|
29
|
+
- 有 JSX 用 `.tsx`,纯逻辑/类型 `.ts`。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "yarch 中后台工程模板 · Arco 档(字节系):Vite + React 19 + react-router 7 + @yarch 契约底座",
|
|
3
|
+
"variables": [
|
|
4
|
+
{ "name": "packageName", "desc": "工程名(= npm 包名 = registry.md 服务名,一-1 校验)", "required": true, "default": "" },
|
|
5
|
+
{ "name": "appName", "desc": "展示名(index.html 标题)", "required": false, "default": "= packageName" },
|
|
6
|
+
{ "name": "description", "desc": "工程描述(package.json)", "required": false, "default": "" },
|
|
7
|
+
{ "name": "port", "desc": "vite dev 端口", "required": false, "default": "5173" },
|
|
8
|
+
{ "name": "proxyTarget", "desc": "/api/v1 反向代理目标", "required": false, "default": "http://localhost:8080" },
|
|
9
|
+
{ "name": "yarchContractDep", "desc": "@yarch/contract 依赖(发版前 file: 本地路径,发版后 ^x.y.z)", "required": false, "default": "" },
|
|
10
|
+
{ "name": "yarchReactDep", "desc": "@yarch/react 依赖(同上)", "required": false, "default": "" }
|
|
11
|
+
]
|
|
12
|
+
}
|