Skip to content

Commit 0276407

Browse files
committed
Feat: 优化模板渲染
1 parent 4f6e47b commit 0276407

4 files changed

Lines changed: 166 additions & 13 deletions

File tree

bin/xcmd.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* @Author: HxB
55
* @Date: 2022-04-25 16:27:06
66
* @LastEditors: DoubleAm
7-
* @LastEditTime: 2025-05-09 14:09:58
7+
* @LastEditTime: 2025-12-26 18:21:45
88
* @Description: 命令处理文件
99
* @FilePath: /js-xcmd/bin/xcmd.js
1010
*/
@@ -790,6 +790,19 @@ program
790790
downloadTpl('http://cdn.biugle.cn/umi_page.zip', dir || '', ['PageCode', 'Author']);
791791
});
792792

793+
program
794+
.option('add-tpl <name> [dir]', 'add-tpl <name> [dir]')
795+
.command('add-tpl <name> [dir]')
796+
.description('创建简单ds页面模板-内部')
797+
.action((name, dir) => {
798+
downloadTpl(
799+
'https://git.imile.com/hank.he/ds-web-tpl/-/archive/main/ds-web-tpl-main.zip',
800+
dir || '',
801+
['PageCode', 'Author'],
802+
`ds-web-tpl-main/${name}`
803+
);
804+
});
805+
793806
program
794807
.command('git2excel <projectCode> [notFilter]')
795808
.description('从 Git 暂存区的 JavaScript/TypeScript 文件中提取 t、$t 或 .t、.$t 方法的参数并转换为 Excel')

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "js-xcmd",
3-
"version": "1.5.17",
3+
"version": "1.5.18",
44
"description": "XCmd library for node.js.",
55
"main": "main.js",
66
"bin": {

utils/tpl.js

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,95 @@ async function downloadFile(url, dest) {
3030
* 解压 ZIP 文件
3131
* @param {string} zipPath - ZIP 文件路径
3232
* @param {string} extractPath - 解压目录
33+
* @param {string} name - 只解压 zip 包中包含的指定目录
34+
* @param {boolean} preserveStructure - 是否保留原目录结构
35+
*
36+
* 示例用法:
37+
* // 解压整个ZIP文件
38+
* await unzipFile('path/to/file.zip', 'extract/path');
39+
*
40+
* // 只解压名为 'src' 的目录
41+
* await unzipFile('path/to/file.zip', 'extract/path', 'xxx/base_page');
42+
*
43+
* // 解压特定子目录 (解压后会保留子目录结构)
44+
* await unzipFile('path/to/file.zip', 'extract/path', 'xxx/base_page', true);
3345
*/
34-
async function unzipFile(zipPath, extractPath) {
35-
return new Promise((resolve, reject) => {
36-
fs.createReadStream(zipPath)
37-
.pipe(unzipper.Extract({ path: extractPath }))
38-
.on('close', resolve)
39-
.on('error', reject);
40-
});
46+
async function unzipFile(zipPath, extractPath, name = '', preserveStructure = false) {
47+
console.log('开始解压模板...', { zipPath, name, extractPath, preserveStructure });
48+
if (name && typeof name === 'string') {
49+
return new Promise((resolve, reject) => {
50+
fs.createReadStream(zipPath)
51+
.pipe(unzipper.Parse())
52+
.on('entry', function (entry) {
53+
let fileName = entry.path.replace(/\\/g, '/');
54+
const type = entry.type;
55+
const normalizedName = name.replace(/\\/g, '/');
56+
const normalizedDir = normalizedName.endsWith('/') ? normalizedName : normalizedName + '/';
57+
const normalizedPath = fileName.startsWith('/') ? fileName.substring(1) : fileName;
58+
const normalizedNamePath = normalizedName.startsWith('/') ? normalizedName.substring(1) : normalizedName;
59+
// 判断是否需要解压
60+
const shouldExtract =
61+
normalizedPath.startsWith(normalizedNamePath) || normalizedPath.startsWith(normalizedDir);
62+
63+
if (shouldExtract) {
64+
// 处理路径:保留结构或扁平化
65+
let relativePath;
66+
if (normalizedPath.startsWith(normalizedNamePath)) {
67+
if (preserveStructure) {
68+
// 保留结构:保留 name 之后的路径
69+
relativePath = normalizedPath.substring(normalizedNamePath.length);
70+
relativePath = relativePath.replace(/^\/+/, '');
71+
relativePath = path.join(normalizedNamePath, relativePath);
72+
} else {
73+
// 扁平化:只保留 name 之后的路径
74+
relativePath = normalizedPath.substring(normalizedNamePath.length);
75+
relativePath = relativePath.replace(/^\/+/, '');
76+
}
77+
} else {
78+
relativePath = fileName;
79+
}
80+
const entryPath = path.join(extractPath, relativePath);
81+
if (type === 'Directory') {
82+
try {
83+
if (relativePath) {
84+
fs.mkdirSync(entryPath, { recursive: true });
85+
}
86+
entry.autodrain();
87+
} catch (err) {
88+
console.error(`创建目录失败 ${entryPath}:`, err.message);
89+
reject(err);
90+
}
91+
} else {
92+
const dir = path.dirname(entryPath);
93+
try {
94+
if (dir !== extractPath) {
95+
fs.mkdirSync(dir, { recursive: true });
96+
}
97+
entry.pipe(fs.createWriteStream(entryPath));
98+
} catch (err) {
99+
console.error(`创建目录失败 ${dir}:`, err.message);
100+
reject(err);
101+
}
102+
}
103+
} else {
104+
entry.autodrain();
105+
}
106+
})
107+
.on('close', resolve)
108+
.on('error', (err) => {
109+
console.error('解压过程中发生错误:', err.message);
110+
reject(err);
111+
});
112+
});
113+
} else {
114+
// 解压整个 zip
115+
return new Promise((resolve, reject) => {
116+
fs.createReadStream(zipPath)
117+
.pipe(unzipper.Extract({ path: extractPath }))
118+
.on('close', resolve)
119+
.on('error', reject);
120+
});
121+
}
41122
}
42123

43124
/**
@@ -110,8 +191,9 @@ function promptUserInputs(questions) {
110191
* @param {string} zipUrl - ZIP 文件 URL
111192
* @param {string} downloadPath - 下载目录路径
112193
* @param {Array<string>} options - 要收集的替换项
194+
* @param {string} name - 模板名称
113195
*/
114-
async function downloadTpl(zipUrl, downloadPath, options) {
196+
async function downloadTpl(zipUrl, downloadPath, options, name) {
115197
let zipFilePath;
116198

117199
try {
@@ -122,15 +204,17 @@ async function downloadTpl(zipUrl, downloadPath, options) {
122204
}
123205

124206
downloadPath = downloadPath || `./${answers.PageCode}`;
125-
const dest = path.resolve(downloadPath);
207+
const dest = path.resolve(
208+
downloadPath?.includes(answers.PageCode) ? downloadPath : path.join(downloadPath, answers.PageCode)
209+
);
126210
zipFilePath = path.join(dest, `template-${Date.now()}.zip`);
127211

128212
await fsPromises.mkdir(dest, { recursive: true }); // 使用 Promise API 创建目录
129213

130214
await downloadFile(zipUrl, zipFilePath);
131215
console.log(`模板已下载到 ${zipFilePath}`);
132216

133-
await unzipFile(zipFilePath, dest);
217+
await unzipFile(zipFilePath, dest, name);
134218
console.log('模板解压完成');
135219

136220
await traverseDirectory(dest, answers); // 递归遍历目录
@@ -144,7 +228,7 @@ async function downloadTpl(zipUrl, downloadPath, options) {
144228
}
145229
}
146230

147-
module.exports = { downloadTpl };
231+
module.exports = { downloadTpl, unzipFile };
148232

149233
// 调用示例
150234
// downloadTpl('http://cdn.biugle.cn/umi_page.zip', '', ['PageCode', 'Author']);

utils/tpl.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
### 定义模板内容
2+
3+
支持如下语法:
4+
- 变量占位符:`[[[ key ]]]`,支持嵌套,如 `[[[ Config.PageTitle ]]]`
5+
- 默认值:`[[[ key ?? 默认值 ]]]`
6+
- 条件渲染:
7+
- 存在变量:`[[#key]] ... [[/key]]`
8+
- 不存在变量:`[[^key]] ... [[/key]]`
9+
- 循环渲染:`[[*array $item $index]] ... [[/array]]`
10+
11+
### 示例模板:
12+
13+
```html
14+
<!DOCTYPE html>
15+
<html lang="zh">
16+
<head>
17+
<meta charset="UTF-8">
18+
<title>[[[ Config.PageTitle ]]]-[[[ Config.TestEmpty ?? 1.0.0 ]]]</title>
19+
</head>
20+
<body>
21+
<h1>[[[SubTitle]]]</h1>
22+
<table border="1">
23+
<thead>
24+
<tr><th>标题</th><th>作者</th><th>发布日期</th></tr>
25+
</thead>
26+
<tbody>
27+
[[*articles $article $index]]
28+
<tr>
29+
<td>[[[article.title ?? 空白标题]]]</td>
30+
<td>[[[article.author.name ?? 未知作者]]]</td>
31+
<td>
32+
[[#article.date]]
33+
[[[article.date]]]
34+
[[/article.date]]
35+
[[^article.date]]
36+
日期未发布
37+
[[/article.date]]
38+
</td>
39+
</tr>
40+
[[/articles]]
41+
</tbody>
42+
</table>
43+
<footer>版权所有-[[[ Author ]]]</footer>
44+
</body>
45+
</html>
46+
```
47+
48+
### 进阶用法
49+
50+
- 支持对象嵌套、数组循环、条件判断、默认值等复杂场景
51+
- 可递归嵌套模板语法
52+
53+
### 注意事项
54+
- 所有模板语法均为双中括号包裹,避免与常见模板引擎冲突
55+
- 默认值语法 `??` 右侧不支持表达式,仅支持静态文本
56+
- 循环、条件语法建议配合对象结构使用

0 commit comments

Comments
 (0)