xw-devtool-cli 1.0.45 → 1.0.46
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 +12 -0
- package/package.json +4 -2
- package/src/commands/numberUpper.js +236 -0
- package/src/index.js +5 -0
- package/src/locales/en.js +35 -0
- package/src/locales/zh.js +35 -0
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
- **UUID**:生成 UUID v4。
|
|
45
45
|
- **中文转拼音**:将汉字转换为不带声调的拼音。
|
|
46
46
|
- **进制转换**:支持二进制、八进制、十进制、十六进制之间的互相转换。
|
|
47
|
+
- **数字转大写**:支持普通数字转中文大写与人民币金额大写,输入支持剪贴板/手动输入,结果直接复制到剪贴板。
|
|
47
48
|
- **颜色转换**:Hex <-> RGB 互转,并在结果中显示颜色条预览(支持透明度棋盘背景),自动复制 Hex。
|
|
48
49
|
- **颜色预览**:输入颜色并在终端显示颜色条,便于快速视觉确认,同时自动复制 Hex 到剪贴板。
|
|
49
50
|
- **变量格式转换**:支持 CamelCase, PascalCase, SnakeCase, KebabCase, ConstantCase 互转。
|
|
@@ -186,6 +187,17 @@ x. 设置 / 语言 (Settings)
|
|
|
186
187
|
- **复制到剪贴板**:自动复制结果,便于粘贴到文档或 Issue 中。
|
|
187
188
|
- **输出为 txt 文件**:可选择保存位置,默认文件名带时间戳,避免覆盖旧文件。
|
|
188
189
|
|
|
190
|
+
### 数字转大写
|
|
191
|
+
- 在菜单中选择 `数字转大写` 进入。
|
|
192
|
+
- 支持两种转换模式:
|
|
193
|
+
- **数字转中文大写**:如 `1203.45` -> `壹仟贰佰零叁点肆伍`。
|
|
194
|
+
- **数字转人民币大写**:如 `1203.45` -> `壹仟贰佰零叁元肆角伍分`(自动按分四舍五入)。
|
|
195
|
+
- 输入来源支持:
|
|
196
|
+
- **从剪贴板读取**
|
|
197
|
+
- **手动输入数字**
|
|
198
|
+
- 输出结果会直接复制到剪贴板。
|
|
199
|
+
- 控制台会同步显示转换结果,便于快速核对。
|
|
200
|
+
|
|
189
201
|
### 3. 占位图生成 (Placeholder Image)
|
|
190
202
|
- 选择 `3` 进入。
|
|
191
203
|
- **模式 1:本地图片文件 (Local Image File)**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xw-devtool-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.46",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "基于node的开发者助手cli",
|
|
6
6
|
"main": "index.js",
|
|
@@ -73,7 +73,9 @@
|
|
|
73
73
|
"commit-message",
|
|
74
74
|
"conventional-commits",
|
|
75
75
|
"directory-tree",
|
|
76
|
-
"file-tree"
|
|
76
|
+
"file-tree",
|
|
77
|
+
"number-to-chinese-uppercase",
|
|
78
|
+
"rmb-uppercase"
|
|
77
79
|
],
|
|
78
80
|
"author": "npmxw",
|
|
79
81
|
"license": "ISC",
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import clipboardy from 'clipboardy';
|
|
3
|
+
import i18next from '../i18n.js';
|
|
4
|
+
import { read } from '../utils/clipboard.js';
|
|
5
|
+
import { selectFromMenu } from '../utils/menu.js';
|
|
6
|
+
|
|
7
|
+
const UPPER_DIGITS = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
|
|
8
|
+
const SECTION_UNITS = ['', '万', '亿', '兆', '京'];
|
|
9
|
+
const POSITION_UNITS = ['', '拾', '佰', '仟'];
|
|
10
|
+
|
|
11
|
+
function normalizeRawNumber(raw) {
|
|
12
|
+
return raw.replace(/,/g, '').trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isValidNumberText(text) {
|
|
16
|
+
return /^-?\d+(\.\d+)?$/.test(text);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function convertFourDigits(section) {
|
|
20
|
+
const padded = section.padStart(4, '0');
|
|
21
|
+
let result = '';
|
|
22
|
+
let needZero = false;
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < 4; i++) {
|
|
25
|
+
const num = Number(padded[i]);
|
|
26
|
+
const unit = POSITION_UNITS[3 - i];
|
|
27
|
+
if (num === 0) {
|
|
28
|
+
needZero = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (needZero && result) {
|
|
32
|
+
result += UPPER_DIGITS[0];
|
|
33
|
+
}
|
|
34
|
+
result += `${UPPER_DIGITS[num]}${unit}`;
|
|
35
|
+
needZero = false;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function convertIntegerPart(integerText) {
|
|
41
|
+
const normalized = integerText.replace(/^0+/, '') || '0';
|
|
42
|
+
if (normalized === '0') {
|
|
43
|
+
return UPPER_DIGITS[0];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sections = [];
|
|
47
|
+
for (let i = normalized.length; i > 0; i -= 4) {
|
|
48
|
+
const start = Math.max(0, i - 4);
|
|
49
|
+
sections.unshift(normalized.slice(start, i));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let result = '';
|
|
53
|
+
let previousZeroSection = false;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < sections.length; i++) {
|
|
56
|
+
const section = sections[i];
|
|
57
|
+
const sectionValue = Number(section);
|
|
58
|
+
const sectionUnit = SECTION_UNITS[sections.length - 1 - i] || '';
|
|
59
|
+
|
|
60
|
+
if (sectionValue === 0) {
|
|
61
|
+
previousZeroSection = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (previousZeroSection && result) {
|
|
66
|
+
result += UPPER_DIGITS[0];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
result += `${convertFourDigits(section)}${sectionUnit}`;
|
|
70
|
+
previousZeroSection = false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result || UPPER_DIGITS[0];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeMoney(numberText) {
|
|
77
|
+
const isNegative = numberText.startsWith('-');
|
|
78
|
+
const absolute = isNegative ? numberText.slice(1) : numberText;
|
|
79
|
+
const [integerPartRaw, decimalPartRaw = ''] = absolute.split('.');
|
|
80
|
+
|
|
81
|
+
let integerPart = integerPartRaw.replace(/^0+/, '') || '0';
|
|
82
|
+
const decimal = decimalPartRaw.padEnd(3, '0').slice(0, 3);
|
|
83
|
+
let jiao = Number(decimal[0]);
|
|
84
|
+
let fen = Number(decimal[1]);
|
|
85
|
+
const third = Number(decimal[2]);
|
|
86
|
+
|
|
87
|
+
if (third >= 5) {
|
|
88
|
+
fen += 1;
|
|
89
|
+
if (fen === 10) {
|
|
90
|
+
fen = 0;
|
|
91
|
+
jiao += 1;
|
|
92
|
+
if (jiao === 10) {
|
|
93
|
+
jiao = 0;
|
|
94
|
+
integerPart = (BigInt(integerPart) + 1n).toString();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
isNegative,
|
|
101
|
+
integerPart,
|
|
102
|
+
jiao,
|
|
103
|
+
fen
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function convertNumberToChineseUpper(rawNumber) {
|
|
108
|
+
const normalized = normalizeRawNumber(rawNumber);
|
|
109
|
+
if (!isValidNumberText(normalized)) {
|
|
110
|
+
throw new Error(i18next.t('numberUpper.invalidNumber'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isNegative = normalized.startsWith('-');
|
|
114
|
+
const absolute = isNegative ? normalized.slice(1) : normalized;
|
|
115
|
+
const [integerPart, decimalPart = ''] = absolute.split('.');
|
|
116
|
+
const integerResult = convertIntegerPart(integerPart || '0');
|
|
117
|
+
const decimalResult = decimalPart
|
|
118
|
+
.split('')
|
|
119
|
+
.map((digit) => UPPER_DIGITS[Number(digit)])
|
|
120
|
+
.join('');
|
|
121
|
+
|
|
122
|
+
let result = integerResult;
|
|
123
|
+
if (decimalResult) {
|
|
124
|
+
result = `${result}${i18next.t('numberUpper.dot')}${decimalResult}`;
|
|
125
|
+
}
|
|
126
|
+
if (isNegative) {
|
|
127
|
+
result = `${i18next.t('numberUpper.negative')}${result}`;
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function convertNumberToRmbUpper(rawNumber) {
|
|
133
|
+
const normalized = normalizeRawNumber(rawNumber);
|
|
134
|
+
if (!isValidNumberText(normalized)) {
|
|
135
|
+
throw new Error(i18next.t('numberUpper.invalidNumber'));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { isNegative, integerPart, jiao, fen } = normalizeMoney(normalized);
|
|
139
|
+
const integerResult = convertIntegerPart(integerPart);
|
|
140
|
+
let result = `${integerResult}${i18next.t('numberUpper.yuan')}`;
|
|
141
|
+
|
|
142
|
+
if (jiao === 0 && fen === 0) {
|
|
143
|
+
result += i18next.t('numberUpper.whole');
|
|
144
|
+
} else if (jiao === 0 && fen > 0) {
|
|
145
|
+
if (integerPart !== '0') {
|
|
146
|
+
result += UPPER_DIGITS[0];
|
|
147
|
+
}
|
|
148
|
+
result += `${UPPER_DIGITS[fen]}${i18next.t('numberUpper.fen')}`;
|
|
149
|
+
} else {
|
|
150
|
+
result += `${UPPER_DIGITS[jiao]}${i18next.t('numberUpper.jiao')}`;
|
|
151
|
+
if (fen > 0) {
|
|
152
|
+
result += `${UPPER_DIGITS[fen]}${i18next.t('numberUpper.fen')}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (isNegative) {
|
|
157
|
+
result = `${i18next.t('numberUpper.negative')}${result}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function resolveInputBySource(source) {
|
|
164
|
+
if (source === 'clipboard') {
|
|
165
|
+
const text = (await read()).trim();
|
|
166
|
+
if (!text) {
|
|
167
|
+
throw new Error(i18next.t('numberUpper.clipboardEmpty'));
|
|
168
|
+
}
|
|
169
|
+
return text;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const answer = await inquirer.prompt([
|
|
173
|
+
{
|
|
174
|
+
type: 'input',
|
|
175
|
+
name: 'number',
|
|
176
|
+
message: i18next.t('numberUpper.enterNumber'),
|
|
177
|
+
validate: (value) => {
|
|
178
|
+
const normalized = normalizeRawNumber(value);
|
|
179
|
+
if (!normalized) {
|
|
180
|
+
return i18next.t('numberUpper.emptyInput');
|
|
181
|
+
}
|
|
182
|
+
return isValidNumberText(normalized) ? true : i18next.t('numberUpper.invalidNumber');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
]);
|
|
186
|
+
return answer.number.trim();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function numberUpperHandler() {
|
|
190
|
+
const mode = await selectFromMenu(
|
|
191
|
+
i18next.t('numberUpper.modeTitle'),
|
|
192
|
+
[
|
|
193
|
+
{ name: i18next.t('numberUpper.modeDigit'), value: 'digit' },
|
|
194
|
+
{ name: i18next.t('numberUpper.modeRmb'), value: 'rmb' }
|
|
195
|
+
],
|
|
196
|
+
true,
|
|
197
|
+
i18next.t('common.back')
|
|
198
|
+
);
|
|
199
|
+
if (mode === '__BACK__') {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const source = await selectFromMenu(
|
|
204
|
+
i18next.t('numberUpper.sourceTitle'),
|
|
205
|
+
[
|
|
206
|
+
{ name: i18next.t('numberUpper.sourceClipboard'), value: 'clipboard' },
|
|
207
|
+
{ name: i18next.t('numberUpper.sourceManual'), value: 'manual' }
|
|
208
|
+
],
|
|
209
|
+
true,
|
|
210
|
+
i18next.t('common.back')
|
|
211
|
+
);
|
|
212
|
+
if (source === '__BACK__') {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const input = await resolveInputBySource(source);
|
|
218
|
+
if (input === '__BACK__') {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const normalized = normalizeRawNumber(input);
|
|
223
|
+
const result = mode === 'rmb'
|
|
224
|
+
? convertNumberToRmbUpper(normalized)
|
|
225
|
+
: convertNumberToChineseUpper(normalized);
|
|
226
|
+
console.log(`\n${i18next.t('numberUpper.resultLabel')} ${result}\n`);
|
|
227
|
+
try {
|
|
228
|
+
await clipboardy.write(result);
|
|
229
|
+
console.log(i18next.t('numberUpper.copied'));
|
|
230
|
+
} catch (copyError) {
|
|
231
|
+
console.error(i18next.t('numberUpper.copyFailed', { message: copyError.message }));
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
console.error(error.message);
|
|
235
|
+
}
|
|
236
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { saveConfig } from './utils/config.js';
|
|
|
6
6
|
|
|
7
7
|
import { urlHandler } from './commands/url.js';
|
|
8
8
|
import { baseConvertHandler } from './commands/baseConvert.js';
|
|
9
|
+
import { numberUpperHandler } from './commands/numberUpper.js';
|
|
9
10
|
import { base64Handler } from './commands/base64.js';
|
|
10
11
|
import { unicodeHandler } from './commands/unicode.js';
|
|
11
12
|
import { imgBase64Handler } from './commands/imgBase64.js';
|
|
@@ -77,6 +78,7 @@ function getFeatures() {
|
|
|
77
78
|
// Encode/Decode & Formatting
|
|
78
79
|
{ name: i18next.t('menu.features.url'), value: 'url' },
|
|
79
80
|
{ name: i18next.t('menu.features.baseConvert'), value: 'baseConvert' },
|
|
81
|
+
{ name: i18next.t('menu.features.numberUpper'), value: 'numberUpper' },
|
|
80
82
|
{ name: i18next.t('menu.features.base64'), value: 'base64' },
|
|
81
83
|
{ name: i18next.t('menu.features.unicode'), value: 'unicode' },
|
|
82
84
|
{ name: i18next.t('menu.features.htmlEntities'), value: 'htmlEntities' },
|
|
@@ -277,6 +279,9 @@ async function handleAction(action) {
|
|
|
277
279
|
case 'baseConvert':
|
|
278
280
|
await baseConvertHandler();
|
|
279
281
|
break;
|
|
282
|
+
case 'numberUpper':
|
|
283
|
+
await numberUpperHandler();
|
|
284
|
+
break;
|
|
280
285
|
case 'base64':
|
|
281
286
|
await base64Handler();
|
|
282
287
|
break;
|
package/src/locales/en.js
CHANGED
|
@@ -53,6 +53,7 @@ export default {
|
|
|
53
53
|
qrcodeDecode: 'QR Code Reader',
|
|
54
54
|
url: 'URL Encode/Decode',
|
|
55
55
|
baseConvert: 'Number Base Converter',
|
|
56
|
+
numberUpper: 'Number to Chinese Uppercase',
|
|
56
57
|
base64: 'String Encode/Decode (Base64)',
|
|
57
58
|
unicode: 'Unicode Encode/Decode',
|
|
58
59
|
htmlEntities: 'HTML Entity Encode/Decode',
|
|
@@ -173,6 +174,40 @@ export default {
|
|
|
173
174
|
error: 'Invalid number for the selected base.',
|
|
174
175
|
copyPrompt: 'Select format to copy'
|
|
175
176
|
},
|
|
177
|
+
numberUpper: {
|
|
178
|
+
modeTitle: 'Select conversion mode',
|
|
179
|
+
modeDigit: 'Number to Chinese uppercase',
|
|
180
|
+
modeRmb: 'Number to RMB uppercase',
|
|
181
|
+
sourceTitle: 'Select input source',
|
|
182
|
+
sourceClipboard: 'Read from clipboard',
|
|
183
|
+
sourceFile: 'Read from local file',
|
|
184
|
+
sourceManual: 'Enter number manually',
|
|
185
|
+
fileInputMethod: 'Select file input method',
|
|
186
|
+
fileInputDialog: 'Select file (dialog)',
|
|
187
|
+
fileInputManual: 'Enter file path manually',
|
|
188
|
+
dialogUnavailable: 'File dialog is unavailable or canceled. Please enter the file path manually.',
|
|
189
|
+
enterFilePath: 'Enter number file path:',
|
|
190
|
+
fileNotFound: 'File does not exist.',
|
|
191
|
+
readFileError: 'Failed to read file: {{message}}',
|
|
192
|
+
clipboardEmpty: 'Clipboard is empty. Please copy the number first.',
|
|
193
|
+
emptyInput: 'Input cannot be empty.',
|
|
194
|
+
enterNumber: 'Enter number (supports negative and decimal):',
|
|
195
|
+
invalidNumber: 'Please enter a valid number (e.g. 123, -45.67)',
|
|
196
|
+
outputMode: 'Select output method',
|
|
197
|
+
outputClipboard: 'Copy to clipboard',
|
|
198
|
+
outputFile: 'Save to file',
|
|
199
|
+
enterOutputPath: 'Enter output file path:',
|
|
200
|
+
outputPathRequired: 'Output file path cannot be empty.',
|
|
201
|
+
negative: 'Negative ',
|
|
202
|
+
dot: ' point ',
|
|
203
|
+
yuan: ' Yuan',
|
|
204
|
+
jiao: ' Jiao',
|
|
205
|
+
fen: ' Fen',
|
|
206
|
+
whole: ' Only',
|
|
207
|
+
resultLabel: 'Result:',
|
|
208
|
+
copied: 'Copied to clipboard',
|
|
209
|
+
copyFailed: 'Failed to copy to clipboard: {{message}}'
|
|
210
|
+
},
|
|
176
211
|
jsonFormat: {
|
|
177
212
|
selectSource: 'Select JSON input source',
|
|
178
213
|
sourceClipboard: 'Read from clipboard',
|
package/src/locales/zh.js
CHANGED
|
@@ -53,6 +53,7 @@ export default {
|
|
|
53
53
|
qrcodeDecode: '二维码识别',
|
|
54
54
|
url: 'URL 编码/解码',
|
|
55
55
|
baseConvert: '进制转换工具',
|
|
56
|
+
numberUpper: '数字转大写',
|
|
56
57
|
base64: '字符串 编码/解码 (Base64)',
|
|
57
58
|
unicode: 'Unicode 编码/解码',
|
|
58
59
|
htmlEntities: 'HTML 实体 编码/解码',
|
|
@@ -173,6 +174,40 @@ export default {
|
|
|
173
174
|
error: '输入的数字对于所选进制无效。',
|
|
174
175
|
copyPrompt: '选择要复制的格式'
|
|
175
176
|
},
|
|
177
|
+
numberUpper: {
|
|
178
|
+
modeTitle: '选择转换模式',
|
|
179
|
+
modeDigit: '数字转中文大写',
|
|
180
|
+
modeRmb: '数字转人民币大写',
|
|
181
|
+
sourceTitle: '选择输入来源',
|
|
182
|
+
sourceClipboard: '从剪贴板读取',
|
|
183
|
+
sourceFile: '从本地文件读取',
|
|
184
|
+
sourceManual: '手动输入数字',
|
|
185
|
+
fileInputMethod: '选择文件读取方式',
|
|
186
|
+
fileInputDialog: '选择文件(对话框)',
|
|
187
|
+
fileInputManual: '手动输入文件路径',
|
|
188
|
+
dialogUnavailable: '文件对话框不可用或已取消,请手动输入文件路径。',
|
|
189
|
+
enterFilePath: '请输入数字文件路径:',
|
|
190
|
+
fileNotFound: '文件不存在。',
|
|
191
|
+
readFileError: '读取文件失败: {{message}}',
|
|
192
|
+
clipboardEmpty: '剪贴板为空,请先复制数字。',
|
|
193
|
+
emptyInput: '输入内容不能为空。',
|
|
194
|
+
enterNumber: '请输入数字(支持负数和小数):',
|
|
195
|
+
invalidNumber: '请输入有效数字(如 123、-45.67)',
|
|
196
|
+
outputMode: '选择输出方式',
|
|
197
|
+
outputClipboard: '复制到剪贴板',
|
|
198
|
+
outputFile: '保存到文件',
|
|
199
|
+
enterOutputPath: '请输入输出文件路径:',
|
|
200
|
+
outputPathRequired: '输出文件路径不能为空。',
|
|
201
|
+
negative: '负',
|
|
202
|
+
dot: '点',
|
|
203
|
+
yuan: '元',
|
|
204
|
+
jiao: '角',
|
|
205
|
+
fen: '分',
|
|
206
|
+
whole: '整',
|
|
207
|
+
resultLabel: '转换结果:',
|
|
208
|
+
copied: '已复制到剪贴板',
|
|
209
|
+
copyFailed: '复制到剪贴板失败: {{message}}'
|
|
210
|
+
},
|
|
176
211
|
jsonFormat: {
|
|
177
212
|
selectSource: '选择 JSON 输入来源',
|
|
178
213
|
sourceClipboard: '从剪贴板读取',
|