Просмотр исходного кода

合并上游部分代码 合并上游PR 引入二维码解析及雪花算法解析

QingFeng 4 месяцев назад
Родитель
Сommit
312e653b84

+ 1 - 0
package.json

@@ -80,6 +80,7 @@
     "pinia": "^2.0.34",
     "plausible-tracker": "^0.3.8",
     "qrcode": "^1.5.1",
+    "qrcode-parser": "^2.1.3",
     "sql-formatter": "^13.0.0",
     "ua-parser-js": "^1.0.35",
     "ulid": "^2.3.0",

+ 1 - 1
src/components/FormatTransformer.vue

@@ -48,7 +48,7 @@ const output = computed(() => transformer.value(input.value));
     monospace
   />
 
-  <div>
+  <div overflow-auto>
     <div mb-5px>
       {{ outputLabel }}
     </div>

+ 1 - 1
src/plugins/i18n.plugin.ts

@@ -23,7 +23,7 @@ const i18n = createI18n({
   locale: 'cn',
   messages,
 });
-
+// i18n.global.locale.value = 'cn';
 export const i18nPlugin: Plugin = {
   install: (app) => {
     app.use(i18n);

+ 4 - 1
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 qrCodeDecoder } from './qr-code-decoder';
+import { tool as snowflakeIdExtractor } from './snowflake-id-extractor';
 import { tool as jsonToJava } from './json-to-java';
 import { tool as asciiTextDrawer } from './ascii-text-drawer';
 import { tool as textToUnicode } from './text-to-unicode';
@@ -128,11 +130,12 @@ export const toolsByCategory: ToolCategory[] = [
       userAgentParser,
       httpStatusCodes,
       jsonDiff,
+      snowflakeIdExtractor,
     ],
   },
   {
     name: 'Images and videos',
-    components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder],
+    components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder, qrCodeDecoder],
   },
   {
     name: 'Development',

+ 2 - 2
src/tools/jwt-parser/jwt-parser.vue

@@ -39,7 +39,7 @@ const validation = useValidation({
             {{ section.title }}
           </th>
           <tr v-for="{ claim, claimDescription, friendlyValue, value } in decodedJWT[section.key]" :key="claim + value">
-            <td class="claims">
+            <td class="claims" style="vertical-align: top;">
               <span font-bold>
                 {{ claim }}
               </span>
@@ -47,7 +47,7 @@ const validation = useValidation({
                 ({{ claimDescription }})
               </span>
             </td>
-            <td>
+            <td style="word-wrap: break-word;word-break: break-all;">
               <span>{{ value }}</span>
               <span v-if="friendlyValue" ml-2 op-70>
                 ({{ friendlyValue }})

+ 17 - 0
src/tools/qr-code-decoder/index.ts

@@ -0,0 +1,17 @@
+/**
+ * author https://github.com/sharevb
+ * form https://github.com/CorentinTh/it-tools/pull/914
+ */
+
+import { Qrcode } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+  name: 'QRCode decoder',
+  path: '/qr-code-decoder',
+  description: '二维码解码',
+  keywords: ['二维码', 'qr-code', '解码', 'reader'],
+  component: () => import('./qr-code-decoder.vue'),
+  icon: Qrcode,
+  createdAt: new Date('2024-01-17'),
+});

+ 4 - 0
src/tools/qr-code-decoder/locales/cn.yml

@@ -0,0 +1,4 @@
+tools:
+  qr-code-decoder:
+    title: "二维码解码"
+    description: "二维码解码"

+ 49 - 0
src/tools/qr-code-decoder/qr-code-decoder.vue

@@ -0,0 +1,49 @@
+<!-- author https://github.com/sharevb -->
+<!-- form https://github.com/CorentinTh/it-tools/pull/914 -->
+<script setup lang="ts">
+import type { Ref } from 'vue';
+import qrcodeParser from 'qrcode-parser';
+import TextareaCopyable from '@/components/TextareaCopyable.vue';
+
+const fileInput = ref() as Ref<File>;
+const qrCode = computedAsync(async () => {
+  try {
+    return (await qrcodeParser(fileInput.value));
+  }
+  catch (e: any) {
+    return e.toString();
+  }
+});
+
+async function onUpload(file: File) {
+  if (file) {
+    fileInput.value = file;
+  }
+}
+</script>
+
+<template>
+  <div>
+    <c-file-upload
+      title="在此处拖放二维码,或单击选择文件"
+      :paste-image="true"
+      @file-upload="onUpload"
+    />
+
+    <n-divider />
+
+    <div>
+      <h3>Decoded</h3>
+      <TextareaCopyable
+        :value="qrCode || ''"
+        :word-wrap="true"
+      />
+    </div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+::v-deep(.n-upload-trigger) {
+  width: 100%;
+}
+</style>

+ 17 - 0
src/tools/snowflake-id-extractor/index.ts

@@ -0,0 +1,17 @@
+/**
+ * author https://github.com/antegral
+ * form https://github.com/CorentinTh/it-tools/pull/1211
+ */
+
+import { Id } from '@vicons/tabler';
+import { defineTool } from '../tool';
+
+export const tool = defineTool({
+  name: '雪花算法ID解析',
+  path: '/snowflake-id-extractor',
+  description: '从雪花ID中提取时间戳、机器ID和序列号',
+  keywords: ['雪花', '序列', '解析'],
+  component: () => import('./snowflake-id-extractor.vue'),
+  icon: Id,
+  createdAt: new Date('2024-07-22'),
+});

+ 4 - 0
src/tools/snowflake-id-extractor/locales/cn.yml

@@ -0,0 +1,4 @@
+tools:
+  snowflake-id-extractor:
+    title: "雪花算法ID解析"
+    description: "从雪花ID中提取时间戳、机器ID和序列号"

+ 4 - 0
src/tools/snowflake-id-extractor/locales/en.yml

@@ -0,0 +1,4 @@
+tools:
+  snowflake-id-extractor:
+    title: "Snowflake ID Extractor"
+    description: "Extract timestamp, machine ID, and sequence number from a Snowflake ID"

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

@@ -0,0 +1,19 @@
+/**
+ * author https://github.com/antegral
+ * form https://github.com/CorentinTh/it-tools/pull/1211
+ */
+import { expect, test } from '@playwright/test';
+
+test.describe('Tool - Snowflake id extractor', () => {
+  test.beforeEach(async ({ page }) => {
+    await page.goto('/snowflake-id-extractor');
+  });
+
+  test('Has correct title', async ({ page }) => {
+    await expect(page).toHaveTitle('Snowflake id extractor - IT Tools');
+  });
+
+  test('', async ({ page }) => {
+
+  });
+});

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

@@ -0,0 +1,18 @@
+/**
+ * author https://github.com/antegral
+ * form https://github.com/CorentinTh/it-tools/pull/1211
+ */
+import { describe, expect, it } from 'vitest';
+import { extractId, extractMachineId, extractTimestamp } from './snowflake-id-extractor.service';
+
+describe('snowflake-id-extractor', () => {
+  it('extract id from Snowflake ID', () => {
+    expect(extractId(1263785187301658678n)).toBe(54);
+  });
+  it('extract machine id from Snowflake ID', () => {
+    expect(extractMachineId(1263785187301658678n)).toBe(65);
+  });
+  it('extract timestamp from Snowflake ID', () => {
+    expect(extractTimestamp(1263785187301658678n, new Date(1420070400000))).toStrictEqual(new Date(1721380268646));
+  });
+});

+ 19 - 0
src/tools/snowflake-id-extractor/snowflake-id-extractor.service.ts

@@ -0,0 +1,19 @@
+/**
+ * author https://github.com/antegral
+ * form https://github.com/CorentinTh/it-tools/pull/1211
+ */
+export function extractId(id: bigint) {
+  return Number(id & 4095n);
+}
+
+export function extractMachineId(id: bigint) {
+  return Number((id >> 12n) & 127n);
+}
+
+export function extractTimestamp(id: bigint, epoch: bigint | Date) {
+  return new Date(Number((id >> 22n) + getTimestamp(epoch)));
+}
+
+function getTimestamp(time: bigint | Date) {
+  return typeof time === 'bigint' ? time : BigInt(new Date(time).getTime());
+}

+ 59 - 0
src/tools/snowflake-id-extractor/snowflake-id-extractor.vue

@@ -0,0 +1,59 @@
+<!-- author https://github.com/antegral -->
+<!-- form https://github.com/CorentinTh/it-tools/pull/1211 -->
+<script setup lang="ts">
+import { extractId, extractMachineId, extractTimestamp } from './snowflake-id-extractor.service';
+
+const inputId = ref('1263785187301658678');
+const inputEpoch = ref('');
+
+const inputProps = {
+  'labelPosition': 'left',
+  'labelWidth': '170px',
+  'labelAlign': 'right',
+  'readonly': true,
+  'mb-2': '',
+} as const;
+</script>
+
+<template>
+  <div>
+    <c-card>
+      <c-input-text v-model:value="inputId" label="Snowflake ID" placeholder="Put Snowflake ID here (eg. 1263785187301658678)" label-position="left" label-width="110px" mb-2 label-align="right" />
+      <c-input-text v-model:value="inputEpoch" label="Epoch" placeholder="Put Epoch Timestamp here (optional, eg. 1420070400000)" label-position="left" label-width="110px" mb-2 label-align="right" />
+
+      <n-divider />
+
+      <InputCopyable
+        label="Local date"
+        v-bind="inputProps"
+        :value="inputEpoch ? new Date(extractTimestamp(BigInt(inputId.valueOf()), BigInt(inputEpoch.valueOf()))).toLocaleString() : ''"
+        placeholder="Epoch Timestamp will be here..."
+        label-position="left" label-width="110px" mb-2 label-align="right"
+      />
+
+      <InputCopyable
+        label="Timestamp"
+        v-bind="inputProps"
+        :value="inputEpoch ? new Date(extractTimestamp(BigInt(inputId.valueOf()), BigInt(inputEpoch.valueOf()))).getTime() : ''"
+        placeholder="Epoch Timestamp will be here..."
+        label-position="left" label-width="110px" mb-2 label-align="right"
+      />
+
+      <InputCopyable
+        label="Machine ID"
+        v-bind="inputProps"
+        :value="extractMachineId(BigInt(inputId.valueOf()))"
+        placeholder="Machine ID will be here..."
+        label-position="left" label-width="110px" mb-2 label-align="right"
+      />
+
+      <InputCopyable
+        label="Sequence"
+        v-bind="inputProps"
+        :value="extractId(BigInt(inputId.valueOf()))"
+        placeholder="Sequence number will be here..."
+        label-position="left" label-width="110px" mb-2 label-align="right"
+      />
+    </c-card>
+  </div>
+</template>

+ 67 - 3
src/ui/c-file-upload/c-file-upload.vue

@@ -5,10 +5,12 @@ const props = withDefaults(defineProps<{
   multiple?: boolean
   accept?: string
   title?: string
+  pasteImage?: boolean
 }>(), {
   multiple: false,
   accept: undefined,
   title: 'Drag and drop files here, or click to select files',
+  pasteImage: false,
 });
 
 const emit = defineEmits<{
@@ -16,11 +18,31 @@ const emit = defineEmits<{
   (event: 'fileUpload', file: File): void
 }>();
 
-const { multiple } = toRefs(props);
+const { multiple, pasteImage } = toRefs(props);
 
 const isOverDropZone = ref(false);
 
+function toBase64(file: File) {
+  return new Promise<string>((resolve, reject) => {
+    const reader = new FileReader();
+    reader.readAsDataURL(file);
+    reader.onload = () => resolve(reader.result?.toString() ?? '');
+    reader.onerror = error => reject(error);
+  });
+}
+
 const fileInput = ref<HTMLInputElement | null>(null);
+const imgPreview = ref<HTMLImageElement | null>(null);
+async function handlePreview(image: File) {
+  if (imgPreview.value) {
+    imgPreview.value.src = await toBase64(image);
+  }
+}
+function clearPreview() {
+  if (imgPreview.value) {
+    imgPreview.value.src = '';
+  }
+}
 
 function triggerFileInput() {
   fileInput.value?.click();
@@ -39,7 +61,30 @@ function handleDrop(event: DragEvent) {
   handleUpload(files);
 }
 
-function handleUpload(files: FileList | null | undefined) {
+async function onPasteImage(evt: ClipboardEvent) {
+  if (!pasteImage.value) {
+    return false;
+  }
+
+  const items = evt.clipboardData?.items;
+  if (!items) {
+    return false;
+  }
+  for (let i = 0; i < items.length; i++) {
+    if (items[i].type.includes('image')) {
+      const imageFile = items[i].getAsFile();
+      if (imageFile) {
+        await handlePreview(imageFile);
+        emit('fileUpload', imageFile);
+      }
+    }
+  }
+  return true;
+}
+
+async function handleUpload(files: FileList | null | undefined) {
+  clearPreview();
+
   if (_.isNil(files) || _.isEmpty(files)) {
     return;
   }
@@ -49,6 +94,7 @@ function handleUpload(files: FileList | null | undefined) {
     return;
   }
 
+  await handlePreview(files[0]);
   emit('fileUpload', files[0]);
 }
 </script>
@@ -60,6 +106,7 @@ function handleUpload(files: FileList | null | undefined) {
       'border-primary border-opacity-100': isOverDropZone,
     }"
     @click="triggerFileInput"
+    @paste.prevent="onPasteImage"
     @drop.prevent="handleDrop"
     @dragover.prevent
     @dragenter="isOverDropZone = true"
@@ -73,6 +120,7 @@ function handleUpload(files: FileList | null | undefined) {
       :accept="accept"
       @change="handleFileInput"
     >
+
     <slot>
       <span op-70>
         {{ title }}
@@ -88,8 +136,24 @@ function handleUpload(files: FileList | null | undefined) {
       </div>
 
       <c-button>
-        浏览文件
+        选择文件
       </c-button>
+
+      <div v-if="pasteImage">
+        <!-- separator -->
+        <div my-4 w-full flex items-center justify-center op-70>
+          <div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" />
+          <div class="mx-2 text-gray-400">
+            或
+          </div>
+          <div class="h-1px max-w-100px flex-1 bg-gray-300 op-50" />
+        </div>
+
+        <p>从剪贴板粘贴图像</p>
+      </div>
     </slot>
+    <div mt-2>
+      <img ref="imgPreview" width="150">
+    </div>
   </div>
 </template>