3 Commity 55bcef2628 ... 4b0080c35a

Autor SHA1 Wiadomość Data
  QingFeng 4b0080c35a update i18n 3 miesięcy temu
  QingFeng aa455eb9c0 同步上游工具类 3 miesięcy temu
  QingFeng 61f2f20088 merge it-tools pr #913 4 miesięcy temu

+ 35 - 0
components.d.ts

@@ -12,6 +12,7 @@ declare module '@vue/runtime-core' {
     '404.page': typeof import('./src/pages/404.page.vue')['default']
     About: typeof import('./src/pages/About.vue')['default']
     App: typeof import('./src/App.vue')['default']
+    AsciiTextDrawer: typeof import('./src/tools/ascii-text-drawer/ascii-text-drawer.vue')['default']
     'Base.layout': typeof import('./src/layouts/base.layout.vue')['default']
     Base64FileConverter: typeof import('./src/tools/base64-file-converter/base64-file-converter.vue')['default']
     Base64StringConverter: typeof import('./src/tools/base64-string-converter/base64-string-converter.vue')['default']
@@ -88,14 +89,27 @@ declare module '@vue/runtime-core' {
     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default']
     IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default']
     'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default']
+    'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default']
     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default']
+    IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default']
+    IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default']
+    IconMdiCamera: typeof import('~icons/mdi/camera')['default']
     IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
     IconMdiClose: typeof import('~icons/mdi/close')['default']
+    IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
+    IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
+    IconMdiDownload: typeof import('~icons/mdi/download')['default']
     IconMdiEye: typeof import('~icons/mdi/eye')['default']
     IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default']
     IconMdiHeart: typeof import('~icons/mdi/heart')['default']
+    IconMdiPause: typeof import('~icons/mdi/pause')['default']
+    IconMdiPlay: typeof import('~icons/mdi/play')['default']
+    IconMdiRecord: typeof import('~icons/mdi/record')['default']
+    IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
     IconMdiSearch: typeof import('~icons/mdi/search')['default']
+    IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default']
+    IconMdiVideo: typeof import('~icons/mdi/video')['default']
     InputCopyable: typeof import('./src/components/InputCopyable.vue')['default']
     IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default']
     Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default']
@@ -108,6 +122,7 @@ declare module '@vue/runtime-core' {
     JsonToGo: typeof import('./src/tools/json-to-go/json-to-go.vue')['default']
     JsonToJava: typeof import('./src/tools/json-to-java/json-to-java.vue')['default']
     JsonToToml: typeof import('./src/tools/json-to-toml/json-to-toml.vue')['default']
+    JsonToXml: typeof import('./src/tools/json-to-xml/json-to-xml.vue')['default']
     JsonToYaml: typeof import('./src/tools/json-to-yaml-converter/json-to-yaml.vue')['default']
     JsonViewer: typeof import('./src/tools/json-viewer/json-viewer.vue')['default']
     JwtParser: typeof import('./src/tools/jwt-parser/jwt-parser.vue')['default']
@@ -124,23 +139,40 @@ declare module '@vue/runtime-core' {
     MenuLayout: typeof import('./src/components/MenuLayout.vue')['default']
     MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default']
     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default']
+    NAlert: typeof import('naive-ui')['NAlert']
     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
+    NCheckbox: typeof import('naive-ui')['NCheckbox']
     NCode: typeof import('naive-ui')['NCode']
     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
+    NColorPicker: typeof import('naive-ui')['NColorPicker']
     NConfigProvider: typeof import('naive-ui')['NConfigProvider']
+    NDatePicker: typeof import('naive-ui')['NDatePicker']
+    NDivider: typeof import('naive-ui')['NDivider']
+    NDynamicInput: typeof import('naive-ui')['NDynamicInput']
     NEllipsis: typeof import('naive-ui')['NEllipsis']
+    NForm: typeof import('naive-ui')['NForm']
     NFormItem: typeof import('naive-ui')['NFormItem']
     NGi: typeof import('naive-ui')['NGi']
     NGrid: typeof import('naive-ui')['NGrid']
     NH1: typeof import('naive-ui')['NH1']
+    NH2: typeof import('naive-ui')['NH2']
     NH3: typeof import('naive-ui')['NH3']
     NIcon: typeof import('naive-ui')['NIcon']
+    NImage: typeof import('naive-ui')['NImage']
+    NInputGroup: typeof import('naive-ui')['NInputGroup']
+    NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
     NInputNumber: typeof import('naive-ui')['NInputNumber']
     NLayout: typeof import('naive-ui')['NLayout']
     NLayoutSider: typeof import('naive-ui')['NLayoutSider']
     NMenu: typeof import('naive-ui')['NMenu']
+    NProgress: typeof import('naive-ui')['NProgress']
     NScrollbar: typeof import('naive-ui')['NScrollbar']
+    NSlider: typeof import('naive-ui')['NSlider']
+    NSpace: typeof import('naive-ui')['NSpace']
+    NSpin: typeof import('naive-ui')['NSpin']
+    NStatistic: typeof import('naive-ui')['NStatistic']
     NSwitch: typeof import('naive-ui')['NSwitch']
+    NTable: typeof import('naive-ui')['NTable']
     NTag: typeof import('naive-ui')['NTag']
     NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
     OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
@@ -149,6 +181,7 @@ declare module '@vue/runtime-core' {
     PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default']
     PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
     PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
+    QrCodeDecoder: typeof import('./src/tools/qr-code-decoder/qr-code-decoder.vue')['default']
     QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
     RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
     ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']
@@ -157,6 +190,7 @@ declare module '@vue/runtime-core' {
     RouterView: typeof import('vue-router')['RouterView']
     RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
     SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
+    SnowflakeIdExtractor: typeof import('./src/tools/snowflake-id-extractor/snowflake-id-extractor.vue')['default']
     SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
     SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']
     StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default']
@@ -182,6 +216,7 @@ declare module '@vue/runtime-core' {
     UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default']
     WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default']
     XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default']
+    XmlToJson: typeof import('./src/tools/xml-to-json/xml-to-json.vue')['default']
     YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default']
     YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default']
     YamlViewer: typeof import('./src/tools/yaml-viewer/yaml-viewer.vue')['default']

+ 1 - 0
package.json

@@ -92,6 +92,7 @@
     "vue-router": "^4.1.6",
     "vue-tsc": "^1.8.1",
     "xml-formatter": "^3.3.2",
+    "xml-js": "^1.6.11",
     "yaml": "^2.2.1"
   },
   "devDependencies": {

+ 16 - 6
pnpm-lock.yaml

@@ -179,6 +179,9 @@ importers:
       xml-formatter:
         specifier: ^3.3.2
         version: 3.3.2
+      xml-js:
+        specifier: ^1.6.11
+        version: 1.6.11
       yaml:
         specifier: ^2.2.1
         version: 2.2.1
@@ -2174,8 +2177,8 @@ packages:
   '@vueuse/shared@10.0.0':
     resolution: {integrity: sha512-Zh3LgJqvUBWVY3SiMvXanTcfAneGbt63QPczBRDNgQ6jd/ehodO9a1lCFzaA6SWJJoI+ugVTjHFYJdoR656DVQ==}
 
-  '@vueuse/shared@10.11.0':
-    resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==}
+  '@vueuse/shared@10.11.1':
+    resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
 
   '@vueuse/shared@10.3.0':
     resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==}
@@ -5216,6 +5219,10 @@ packages:
     resolution: {integrity: sha512-ld34F1b7+2UQGNkfsAV4MN3/b7cdUstyMj3XJhzKFasOPtMToVCkqmrNcmrRuSlPxgH1K9tXPkqr75gAT3ix2g==}
     engines: {node: '>= 14'}
 
+  xml-js@1.6.11:
+    resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
+    hasBin: true
+
   xml-name-validator@4.0.0:
     resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
     engines: {node: '>=12'}
@@ -7258,7 +7265,7 @@ snapshots:
     dependencies:
       '@unhead/dom': 0.5.1
       '@unhead/schema': 0.5.1
-      '@vueuse/shared': 10.11.0(vue@3.3.4)
+      '@vueuse/shared': 10.11.1(vue@3.3.4)
       unhead: 0.5.1
       vue: 3.3.4
     transitivePeerDependencies:
@@ -7716,7 +7723,7 @@ snapshots:
       - '@vue/composition-api'
       - vue
 
-  '@vueuse/shared@10.11.0(vue@3.3.4)':
+  '@vueuse/shared@10.11.1(vue@3.3.4)':
     dependencies:
       vue-demi: 0.14.8(vue@3.3.4)
     transitivePeerDependencies:
@@ -10174,8 +10181,7 @@ snapshots:
 
   safer-buffer@2.1.2: {}
 
-  sax@1.2.4:
-    optional: true
+  sax@1.2.4: {}
 
   saxes@6.0.0:
     dependencies:
@@ -11142,6 +11148,10 @@ snapshots:
     dependencies:
       xml-parser-xo: 4.0.5
 
+  xml-js@1.6.11:
+    dependencies:
+      sax: 1.2.4
+
   xml-name-validator@4.0.0: {}
 
   xml-parser-xo@4.0.5: {}

+ 32 - 1
src/composable/queryParams.ts

@@ -1,7 +1,8 @@
 import { useRouteQuery } from '@vueuse/router';
 import { computed } from 'vue';
+import { useStorage } from '@vueuse/core';
 
-export { useQueryParam };
+export { useQueryParam, useQueryParamOrStorage };
 
 const transformers = {
   number: {
@@ -16,6 +17,12 @@ const transformers = {
     fromQuery: (value: string) => value.toLowerCase() === 'true',
     toQuery: (value: boolean) => (value ? 'true' : 'false'),
   },
+  object: {
+    fromQuery: (value: string) => {
+      return JSON.parse(value);
+    },
+    toQuery: (value: object) => JSON.stringify(value),
+  },
 };
 
 function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue: T }) {
@@ -33,3 +40,27 @@ function useQueryParam<T>({ name, defaultValue }: { name: string; defaultValue:
     },
   });
 }
+
+function useQueryParamOrStorage<T>({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) {
+  const type = typeof defaultValue;
+  const transformer = transformers[type as keyof typeof transformers] ?? transformers.string;
+
+  const storageRef = useStorage(storageName, defaultValue);
+  const proxyDefaultValue = transformer.toQuery(defaultValue as never);
+  const proxy = useRouteQuery(name, proxyDefaultValue);
+
+  const r = ref(defaultValue);
+
+  watch(r,
+    (value) => {
+      proxy.value = transformer.toQuery(value as never);
+      storageRef.value = value as never;
+    },
+    { deep: true });
+
+  r.value = (proxy.value && proxy.value !== proxyDefaultValue
+    ? transformer.fromQuery(proxy.value) as unknown as T
+    : storageRef.value as T) as never;
+
+  return r;
+}

+ 4 - 0
src/tools/index.ts

@@ -1,6 +1,8 @@
 import { tool as base64FileConverter } from './base64-file-converter';
 import { tool as base64StringConverter } from './base64-string-converter';
 import { tool as basicAuthGenerator } from './basic-auth-generator';
+import { tool as jsonToXml } from './json-to-xml';
+import { tool as xmlToJson } from './xml-to-json';
 import { tool as qrCodeDecoder } from './qr-code-decoder';
 import { tool as snowflakeIdExtractor } from './snowflake-id-extractor';
 import { tool as jsonToJava } from './json-to-java';
@@ -110,6 +112,8 @@ export const toolsByCategory: ToolCategory[] = [
       listConverter,
       tomlToJson,
       tomlToYaml,
+      xmlToJson,
+      jsonToXml,
     ],
   },
   {

+ 12 - 0
src/tools/json-to-xml/index.ts

@@ -0,0 +1,12 @@
+import { Braces } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+  name: 'JSON 转换 XML',
+  path: '/json-to-xml',
+  description: '将JSON转换为XML',
+  keywords: ['json', 'xml'],
+  component: () => import('./json-to-xml.vue'),
+  icon: Braces,
+  createdAt: new Date('2024-08-09'),
+});

+ 32 - 0
src/tools/json-to-xml/json-to-xml.vue

@@ -0,0 +1,32 @@
+<script setup lang="ts">
+import convert from 'xml-js';
+import JSON5 from 'json5';
+import { withDefaultOnError } from '@/utils/defaults';
+import type { UseValidationRule } from '@/composable/validation';
+
+const defaultValue = '{"a":{"_attributes":{"x":"1.234","y":"It\'s"}}}';
+function transformer(value: string) {
+  return withDefaultOnError(() => {
+    return convert.js2xml(JSON5.parse(value), { compact: true });
+  }, '');
+}
+
+const rules: UseValidationRule<string>[] = [
+  {
+    validator: (v: string) => v === '' || JSON5.parse(v),
+    message: '提供的JSON无效。',
+  },
+];
+</script>
+
+<template>
+  <format-transformer
+    input-label="您的JSON内容"
+    :input-default="defaultValue"
+    input-placeholder="将JSON内容粘贴到此处。。。"
+    output-label="已转换的XML"
+    output-language="xml"
+    :transformer="transformer"
+    :input-validation-rules="rules"
+  />
+</template>

+ 5 - 0
src/tools/json-to-xml/locales/cn.yml

@@ -0,0 +1,5 @@
+tools:
+  json-to-xml:
+    title: 'JSON 转换 XML'
+    description: '将JSON转换为XML'
+

+ 1 - 1
src/tools/snowflake-id-extractor/snowflake-id-extractor.e2e.spec.ts

@@ -16,4 +16,4 @@ test.describe('Tool - Snowflake id extractor', () => {
   test('', async ({ page }) => {
 
   });
-});
+});

+ 1 - 1
src/tools/snowflake-id-extractor/snowflake-id-extractor.service.test.ts

@@ -15,4 +15,4 @@ describe('snowflake-id-extractor', () => {
   it('extract timestamp from Snowflake ID', () => {
     expect(extractTimestamp(1263785187301658678n, new Date(1420070400000))).toStrictEqual(new Date(1721380268646));
   });
-});
+});

+ 1 - 1
src/tools/snowflake-id-extractor/snowflake-id-extractor.vue

@@ -56,4 +56,4 @@ const inputProps = {
       />
     </c-card>
   </div>
-</template>
+</template>

+ 14 - 0
src/tools/token-generator/token-generator.service.test.ts

@@ -94,5 +94,19 @@ describe('token-generator', () => {
       expect(token).toHaveLength(256);
       expect(token).toMatch(/^[a-zA-Z]+$/);
     });
+
+    it('should generate a random string with just numbers except 1 and 2 if only withNumbers is set and deniedChars contains 1 and 2', () => {
+      const token = createToken({
+        withLowercase: false,
+        withUppercase: false,
+        withNumbers: true,
+        withSymbols: false,
+        length: 256,
+        deniedChars: '12',
+      });
+
+      expect(token).toHaveLength(256);
+      expect(token).toMatch(/^[0-9]+$/);
+    });
   });
 });

+ 3 - 1
src/tools/token-generator/token-generator.service.ts

@@ -5,6 +5,7 @@ export function createToken({
   withLowercase = true,
   withNumbers = true,
   withSymbols = false,
+  deniedChars = '',
   length = 64,
   alphabet,
 }: {
@@ -12,6 +13,7 @@ export function createToken({
   withLowercase?: boolean
   withNumbers?: boolean
   withSymbols?: boolean
+  deniedChars?: string
   length?: number
   alphabet?: string
 }) {
@@ -20,7 +22,7 @@ export function createToken({
     withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '',
     withNumbers ? '0123456789' : '',
     withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '',
-  ].join(''); ;
+  ].filter(c => !(deniedChars?.includes(c))).join(''); ;
 
   return shuffleString(allAlphabet.repeat(length)).substring(0, length);
 }

+ 51 - 39
src/tools/token-generator/token-generator.tool.vue

@@ -1,76 +1,88 @@
 <script setup lang="ts">
 import { createToken } from './token-generator.service';
 import { useCopy } from '@/composable/copy';
-import { useQueryParam } from '@/composable/queryParams';
+import { useQueryParamOrStorage } from '@/composable/queryParams';
 import { computedRefreshable } from '@/composable/computedRefreshable';
 
-const length = useQueryParam({ name: 'length', defaultValue: 64 });
-const withUppercase = useQueryParam({ name: 'uppercase', defaultValue: true });
-const withLowercase = useQueryParam({ name: 'lowercase', defaultValue: true });
-const withNumbers = useQueryParam({ name: 'numbers', defaultValue: true });
-const withSymbols = useQueryParam({ name: 'symbols', defaultValue: false });
+const count = useQueryParamOrStorage({ name: 'count', storageName: 'token-generator:count', defaultValue: 1 });
+const length = useQueryParamOrStorage({ name: 'length', storageName: 'token-generator:length', defaultValue: 64 });
+const withUppercase = useQueryParamOrStorage({ name: 'uppercase', storageName: 'token-generator:uppercase', defaultValue: true });
+const withLowercase = useQueryParamOrStorage({ name: 'lowercase', storageName: 'token-generator:lowercase', defaultValue: true });
+const withNumbers = useQueryParamOrStorage({ name: 'numbers', storageName: 'token-generator:numbers', defaultValue: true });
+const withSymbols = useQueryParamOrStorage({ name: 'symbols', storageName: 'token-generator:symbols', defaultValue: false });
+const deniedChars = useQueryParamOrStorage({ name: 'deny', storageName: 'token-generator:deny', defaultValue: '' });
 const { t } = useI18n();
 
-const [token, refreshToken] = computedRefreshable(() =>
-  createToken({
-    length: length.value,
-    withUppercase: withUppercase.value,
-    withLowercase: withLowercase.value,
-    withNumbers: withNumbers.value,
-    withSymbols: withSymbols.value,
-  }),
+const [tokens, refreshTokens] = computedRefreshable(() =>
+  Array.from({ length: count.value },
+    () => createToken({
+      length: length.value,
+      withUppercase: withUppercase.value,
+      withLowercase: withLowercase.value,
+      withNumbers: withNumbers.value,
+      withSymbols: withSymbols.value,
+      deniedChars: deniedChars.value,
+    })).join('\n'),
 );
 
-const { copy } = useCopy({ source: token, text: t('tools.token-generator.copied') });
+const { copy } = useCopy({ source: tokens, text: t('tools.token-generator.copyclipboard') });
 </script>
 
 <template>
   <div>
     <c-card>
       <n-form label-placement="left" label-width="140">
-        <div flex justify-center>
-          <div>
-            <n-form-item :label="t('tools.token-generator.uppercase')">
-              <n-switch v-model:value="withUppercase" />
-            </n-form-item>
+        <n-space justify="center">
+          <n-form-item :label="t('tools.token-generator.uppercase')">
+            <n-switch v-model:value="withUppercase" />
+          </n-form-item>
 
-            <n-form-item :label="t('tools.token-generator.lowercase')">
-              <n-switch v-model:value="withLowercase" />
-            </n-form-item>
-          </div>
+          <n-form-item :label="t('tools.token-generator.lowercase')">
+            <n-switch v-model:value="withLowercase" />
+          </n-form-item>
+          <n-form-item :label="t('tools.token-generator.numbers')">
+            <n-switch v-model:value="withNumbers" />
+          </n-form-item>
 
-          <div>
-            <n-form-item :label="t('tools.token-generator.numbers')">
-              <n-switch v-model:value="withNumbers" />
-            </n-form-item>
-
-            <n-form-item :label="t('tools.token-generator.symbols')">
-              <n-switch v-model:value="withSymbols" />
-            </n-form-item>
-          </div>
-        </div>
+          <n-form-item :label="t('tools.token-generator.symbols')">
+            <n-switch v-model:value="withSymbols" />
+          </n-form-item>
+        </n-space>
       </n-form>
 
+      <n-form-item label="排除字符" label-placement="left">
+        <c-input-text
+          v-model:value="deniedChars"
+          placeholder="输入需要在token中排除的字符"
+        />
+      </n-form-item>
+
       <n-form-item :label="`${t('tools.token-generator.length')} (${length})`" label-placement="left">
-        <n-slider v-model:value="length" :step="1" :min="1" :max="512" />
+        <n-slider v-model:value="length" :step="1" :min="1" :max="512" mr-2 />
+        <n-input-number v-model:value="length" size="small" />
+      </n-form-item>
+
+      <n-form-item label="Number of token to generate" label-placement="left">
+        <n-input-number v-model:value="count" size="small" />
       </n-form-item>
 
       <c-input-text
-        v-model:value="token"
+        v-model:value="tokens"
         multiline
         :placeholder="t('tools.token-generator.tokenPlaceholder')"
         readonly
         rows="3"
         autosize
         class="token-display"
+        word-wrap
       />
 
       <div mt-5 flex justify-center gap-3>
         <c-button @click="copy()">
-          {{ t('common.operate.copy') }}
+        {{ t('common.operate.copy') }}
         </c-button>
-        <c-button @click="refreshToken">
-          {{ t('common.operate.refresh') }}
+        <c-button @click="refreshTokens">
+        {{ t('common.operate.refresh') }}
         </c-button>
       </div>
     </c-card>

+ 12 - 0
src/tools/xml-to-json/index.ts

@@ -0,0 +1,12 @@
+import { Braces } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+  name: 'XML 转换 JSON',
+  path: '/xml-to-json',
+  description: '将XML转换为JSON',
+  keywords: ['xml', 'json'],
+  component: () => import('./xml-to-json.vue'),
+  icon: Braces,
+  createdAt: new Date('2024-08-09'),
+});

+ 5 - 0
src/tools/xml-to-json/locales/cn.yml

@@ -0,0 +1,5 @@
+tools:
+  xml-to-json:
+    title: 'XML 转换 JSON'
+    description: '将XML转换为JSON'
+

+ 32 - 0
src/tools/xml-to-json/xml-to-json.vue

@@ -0,0 +1,32 @@
+<script setup lang="ts">
+import convert from 'xml-js';
+import { isValidXML } from '../xml-formatter/xml-formatter.service';
+import { withDefaultOnError } from '@/utils/defaults';
+import type { UseValidationRule } from '@/composable/validation';
+
+const defaultValue = '<a x="1.234" y="It\'s"/>';
+function transformer(value: string) {
+  return withDefaultOnError(() => {
+    return JSON.stringify(convert.xml2js(value, { compact: true }), null, 2);
+  }, '');
+}
+
+const rules: UseValidationRule<string>[] = [
+  {
+    validator: isValidXML,
+    message: '提供的XML无效。',
+  },
+];
+</script>
+
+<template>
+  <format-transformer
+    input-label="您的XML内容"
+    :input-default="defaultValue"
+    input-placeholder="将XML内容粘贴到此处。。。"
+    output-label="转换后的JSON"
+    output-language="json"
+    :transformer="transformer"
+    :input-validation-rules="rules"
+  />
+</template>