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 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.45",
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: '从剪贴板读取',