mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-05-05 13:57:10 -04:00
WIP(translate): translate web and math category all tools
This commit is contained in:
parent
4f550a9499
commit
a2e498d0aa
37 changed files with 406 additions and 119 deletions
|
@ -1,6 +1,7 @@
|
||||||
import { formatDuration } from 'date-fns';
|
import { formatDuration } from 'date-fns';
|
||||||
|
import type { Locale } from 'date-fns';
|
||||||
|
|
||||||
export function formatMsDuration(duration: number) {
|
export function formatMsDuration(duration: number, locale: Locale) {
|
||||||
const ms = Math.floor(duration % 1000);
|
const ms = Math.floor(duration % 1000);
|
||||||
const secs = Math.floor(((duration - ms) / 1000) % 60);
|
const secs = Math.floor(((duration - ms) / 1000) % 60);
|
||||||
const mins = Math.floor((((duration - ms) / 1000 - secs) / 60) % 60);
|
const mins = Math.floor((((duration - ms) / 1000 - secs) / 60) % 60);
|
||||||
|
@ -11,6 +12,6 @@ export function formatMsDuration(duration: number) {
|
||||||
hours: hrs,
|
hours: hrs,
|
||||||
minutes: mins,
|
minutes: mins,
|
||||||
seconds: secs,
|
seconds: secs,
|
||||||
}) + (ms > 0 ? ` ${ms} ms` : '')
|
}, { locale }) + (ms > 0 ? ` ${ms} ms` : '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,57 +3,58 @@
|
||||||
|
|
||||||
import { addMilliseconds, formatRelative } from 'date-fns';
|
import { addMilliseconds, formatRelative } from 'date-fns';
|
||||||
|
|
||||||
import { enGB } from 'date-fns/locale';
|
import { enGB, zhCN } from 'date-fns/locale';
|
||||||
|
|
||||||
import { formatMsDuration } from './eta-calculator.service';
|
import { formatMsDuration } from './eta-calculator.service';
|
||||||
|
|
||||||
|
const { t, locale } = useI18n();
|
||||||
const unitCount = ref(3 * 62);
|
const unitCount = ref(3 * 62);
|
||||||
const unitPerTimeSpan = ref(3);
|
const unitPerTimeSpan = ref(3);
|
||||||
const timeSpan = ref(5);
|
const timeSpan = ref(5);
|
||||||
const timeSpanUnitMultiplier = ref(60000);
|
const timeSpanUnitMultiplier = ref(60000);
|
||||||
const startedAt = ref(Date.now());
|
const startedAt = ref(Date.now());
|
||||||
|
|
||||||
|
const localeLang = computed(() => locale.value === 'zh' ? zhCN : enGB);
|
||||||
const durationMs = computed(() => {
|
const durationMs = computed(() => {
|
||||||
const timeSpanMs = timeSpan.value * timeSpanUnitMultiplier.value;
|
const timeSpanMs = timeSpan.value * timeSpanUnitMultiplier.value;
|
||||||
|
|
||||||
return unitCount.value / (unitPerTimeSpan.value / timeSpanMs);
|
return unitCount.value / (unitPerTimeSpan.value / timeSpanMs);
|
||||||
});
|
});
|
||||||
const endAt = computed(() =>
|
const endAt = computed(() =>
|
||||||
formatRelative(addMilliseconds(startedAt.value, durationMs.value), Date.now(), { locale: enGB }),
|
formatRelative(addMilliseconds(startedAt.value, durationMs.value), Date.now(), { locale: localeLang.value }),
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div text-justify op-70>
|
<div text-justify op-70>
|
||||||
With a concrete example, if you wash 5 plates in 3 minutes and you have 500 plates to wash, it will take you 5
|
{{ t('tools.eta-calculator.tips') }}
|
||||||
hours to wash them all.
|
|
||||||
</div>
|
</div>
|
||||||
<n-divider />
|
<n-divider />
|
||||||
<div flex gap-2>
|
<div flex gap-2>
|
||||||
<n-form-item label="Amount of element to consume" flex-1>
|
<n-form-item :label="t('tools.eta-calculator.unitCount')" flex-1>
|
||||||
<n-input-number v-model:value="unitCount" :min="1" />
|
<n-input-number v-model:value="unitCount" :min="1" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="The consumption started at" flex-1>
|
<n-form-item :label="t('tools.eta-calculator.startedAt')" flex-1>
|
||||||
<n-date-picker v-model:value="startedAt" type="datetime" />
|
<n-date-picker v-model:value="startedAt" type="datetime" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>Amount of unit consumed by time span</p>
|
<p>{{ t('tools.eta-calculator.unitPerTimeSpan') }}</p>
|
||||||
<div flex flex-col items-baseline gap-y-2 md:flex-row>
|
<div flex flex-col items-baseline gap-y-2 md:flex-row>
|
||||||
<n-input-number v-model:value="unitPerTimeSpan" :min="1" />
|
<n-input-number v-model:value="unitPerTimeSpan" :min="1" />
|
||||||
<div flex items-baseline gap-2>
|
<div flex items-baseline gap-2>
|
||||||
<span ml-2>in</span>
|
<span ml-2>{{ t('tools.eta-calculator.in') }}</span>
|
||||||
<n-input-number v-model:value="timeSpan" min-w-130px :min="1" />
|
<n-input-number v-model:value="timeSpan" min-w-130px :min="1" />
|
||||||
<c-select
|
<c-select
|
||||||
v-model:value="timeSpanUnitMultiplier"
|
v-model:value="timeSpanUnitMultiplier"
|
||||||
min-w-130px
|
min-w-130px
|
||||||
:options="[
|
:options="[
|
||||||
{ label: 'milliseconds', value: 1 },
|
{ label: t('tools.eta-calculator.milliseconds'), value: 1 },
|
||||||
{ label: 'seconds', value: 1000 },
|
{ label: t('tools.eta-calculator.seconds'), value: 1000 },
|
||||||
{ label: 'minutes', value: 1000 * 60 },
|
{ label: t('tools.eta-calculator.minutes'), value: 1000 * 60 },
|
||||||
{ label: 'hours', value: 1000 * 60 * 60 },
|
{ label: t('tools.eta-calculator.hours'), value: 1000 * 60 * 60 },
|
||||||
{ label: 'days', value: 1000 * 60 * 60 * 24 },
|
{ label: t('tools.eta-calculator.days'), value: 1000 * 60 * 60 * 24 },
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,12 +62,12 @@ const endAt = computed(() =>
|
||||||
|
|
||||||
<n-divider />
|
<n-divider />
|
||||||
<c-card mb-2>
|
<c-card mb-2>
|
||||||
<n-statistic label="Total duration">
|
<n-statistic :label="t('tools.eta-calculator.totalDuration')">
|
||||||
{{ formatMsDuration(durationMs) }}
|
{{ formatMsDuration(durationMs, localeLang) }}
|
||||||
</n-statistic>
|
</n-statistic>
|
||||||
</c-card>
|
</c-card>
|
||||||
<c-card>
|
<c-card>
|
||||||
<n-statistic label="It will end ">
|
<n-statistic :label="t('tools.eta-calculator.endAt')">
|
||||||
{{ endAt }}
|
{{ endAt }}
|
||||||
</n-statistic>
|
</n-statistic>
|
||||||
</c-card>
|
</c-card>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Hourglass } from '@vicons/tabler';
|
import { Hourglass } from '@vicons/tabler';
|
||||||
import { defineTool } from '../tool';
|
import { defineTool } from '../tool';
|
||||||
|
import { translate as t } from '@/plugins/i18n.plugin';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'ETA calculator',
|
name: t('tools.eta-calculator.title'),
|
||||||
path: '/eta-calculator',
|
path: '/eta-calculator',
|
||||||
description:
|
description: t('tools.eta-calculator.description'),
|
||||||
'An ETA (Estimated Time of Arrival) calculator to know the approximate end time of a task, for example the moment of ending of a download.',
|
|
||||||
keywords: ['eta', 'calculator', 'estimated', 'time', 'arrival', 'average'],
|
keywords: ['eta', 'calculator', 'estimated', 'time', 'arrival', 'average'],
|
||||||
component: () => import('./eta-calculator.vue'),
|
component: () => import('./eta-calculator.vue'),
|
||||||
icon: Hourglass,
|
icon: Hourglass,
|
||||||
|
|
17
src/tools/eta-calculator/locales/en.yml
Normal file
17
src/tools/eta-calculator/locales/en.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
tools:
|
||||||
|
eta-calculator:
|
||||||
|
title: ETA calculator
|
||||||
|
description: An ETA (Estimated Time of Arrival) calculator to know the approximate end time of a task, for example the moment of ending of a download.
|
||||||
|
|
||||||
|
tips: With a concrete example, if you wash 5 plates in 3 minutes and you have 500 plates to wash, it will take you 5 hours to wash them all.
|
||||||
|
unitCount: Amount of element to consume
|
||||||
|
startedAt: The consumption started at
|
||||||
|
unitPerTimeSpan: Amount of unit consumed by time span
|
||||||
|
in: in
|
||||||
|
milliseconds: milliseconds
|
||||||
|
seconds: seconds
|
||||||
|
minutes: minutes
|
||||||
|
hours: hours
|
||||||
|
days: days
|
||||||
|
totalDuration: Total duration
|
||||||
|
endAt: It will end
|
17
src/tools/eta-calculator/locales/zh.yml
Normal file
17
src/tools/eta-calculator/locales/zh.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
tools:
|
||||||
|
eta-calculator:
|
||||||
|
title: 预计到达时间计算器
|
||||||
|
description: 一个预计到达时间(ETA)计算器,用于了解任务的大致结束时间,例如下载结束的时间。
|
||||||
|
|
||||||
|
tips: 以一个具体的例子来说,如果你用 3 分钟洗 5 个盘子,而你有 500 个盘子要洗,那么你需要 5 个小时来洗完所有盘子。
|
||||||
|
unitCount: 要消耗的元素数量
|
||||||
|
startedAt: 消耗开始于
|
||||||
|
unitPerTimeSpan: 每个时间段消耗的单位数量
|
||||||
|
in: 用
|
||||||
|
milliseconds: 毫秒
|
||||||
|
seconds: 秒
|
||||||
|
minutes: 分钟
|
||||||
|
hours: 小时
|
||||||
|
days: 天
|
||||||
|
totalDuration: 总持续时间
|
||||||
|
endAt: 它将在以下时间结束
|
|
@ -1,10 +1,11 @@
|
||||||
import { Binary } from '@vicons/tabler';
|
import { Binary } from '@vicons/tabler';
|
||||||
import { defineTool } from '../tool';
|
import { defineTool } from '../tool';
|
||||||
|
import { translate as t } from '@/plugins/i18n.plugin';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'Ipv4 address converter',
|
name: t('tools.ipv4-address-converter.title'),
|
||||||
path: '/ipv4-address-converter',
|
path: '/ipv4-address-converter',
|
||||||
description: 'Convert an ip address into decimal, binary, hexadecimal or event in ipv6',
|
description: t('tools.ipv4-address-converter.description'),
|
||||||
keywords: ['ipv4', 'address', 'converter', 'decimal', 'hexadecimal', 'binary', 'ipv6'],
|
keywords: ['ipv4', 'address', 'converter', 'decimal', 'hexadecimal', 'binary', 'ipv6'],
|
||||||
component: () => import('./ipv4-address-converter.vue'),
|
component: () => import('./ipv4-address-converter.vue'),
|
||||||
icon: Binary,
|
icon: Binary,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { convertBase } from '../integer-base-converter/integer-base-converter.mo
|
||||||
import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service';
|
import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service';
|
||||||
import { useValidation } from '@/composable/validation';
|
import { useValidation } from '@/composable/validation';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1');
|
const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1');
|
||||||
|
|
||||||
const convertedSections = computed(() => {
|
const convertedSections = computed(() => {
|
||||||
|
@ -10,23 +11,23 @@ const convertedSections = computed(() => {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Decimal: ',
|
label: t('tools.ipv4-address-converter.decimal'),
|
||||||
value: String(ipInDecimal),
|
value: String(ipInDecimal),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Hexadecimal: ',
|
label: t('tools.ipv4-address-converter.hexadecimal'),
|
||||||
value: convertBase({ fromBase: 10, toBase: 16, value: String(ipInDecimal) }).toUpperCase(),
|
value: convertBase({ fromBase: 10, toBase: 16, value: String(ipInDecimal) }).toUpperCase(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Binary: ',
|
label: t('tools.ipv4-address-converter.binary'),
|
||||||
value: convertBase({ fromBase: 10, toBase: 2, value: String(ipInDecimal) }),
|
value: convertBase({ fromBase: 10, toBase: 2, value: String(ipInDecimal) }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Ipv6: ',
|
label: t('tools.ipv4-address-converter.ipv6'),
|
||||||
value: ipv4ToIpv6({ ip: rawIpAddress.value }),
|
value: ipv4ToIpv6({ ip: rawIpAddress.value }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Ipv6 (short): ',
|
label: t('tools.ipv4-address-converter.ipv6Short'),
|
||||||
value: ipv4ToIpv6({ ip: rawIpAddress.value, prefix: '::ffff:' }),
|
value: ipv4ToIpv6({ ip: rawIpAddress.value, prefix: '::ffff:' }),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -34,13 +35,13 @@ const convertedSections = computed(() => {
|
||||||
|
|
||||||
const { attrs: validationAttrs } = useValidation({
|
const { attrs: validationAttrs } = useValidation({
|
||||||
source: rawIpAddress,
|
source: rawIpAddress,
|
||||||
rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }],
|
rules: [{ message: t('tools.ipv4-address-converter.invalidMessage'), validator: ip => isValidIpv4({ ip }) }],
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." />
|
<c-input-text v-model:value="rawIpAddress" :label="t('tools.ipv4-address-converter.ipv4AddressLabel')" :placeholder="t('tools.ipv4-address-converter.ipv4AddressPlaceholder')" />
|
||||||
|
|
||||||
<n-divider />
|
<n-divider />
|
||||||
|
|
||||||
|
@ -49,11 +50,11 @@ const { attrs: validationAttrs } = useValidation({
|
||||||
:key="label"
|
:key="label"
|
||||||
:label="label"
|
:label="label"
|
||||||
label-position="left"
|
label-position="left"
|
||||||
label-width="100px"
|
label-width="120px"
|
||||||
label-align="right"
|
label-align="right"
|
||||||
mb-2
|
mb-2
|
||||||
:value="validationAttrs.validationStatus === 'error' ? '' : value"
|
:value="validationAttrs.validationStatus === 'error' ? '' : value"
|
||||||
placeholder="Set a correct ipv4 address"
|
:placeholder="t('tools.ipv4-address-converter.errorPlaceholder')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
15
src/tools/ipv4-address-converter/locales/en.yml
Normal file
15
src/tools/ipv4-address-converter/locales/en.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
tools:
|
||||||
|
ipv4-address-converter:
|
||||||
|
title: Ipv4 address converter
|
||||||
|
description: Convert an ip address into decimal, binary, hexadecimal or event in ipv6
|
||||||
|
|
||||||
|
ipv4AddressLabel: 'The ipv4 address:'
|
||||||
|
ipv4AddressPlaceholder: 'The ipv4 address...'
|
||||||
|
decimal: 'Decimal: '
|
||||||
|
hexadecimal: 'Hexadecimal: '
|
||||||
|
binary: 'Binary: '
|
||||||
|
ipv6: 'Ipv6: '
|
||||||
|
ipv6Short: 'Ipv6 (short): '
|
||||||
|
|
||||||
|
errorPlaceholder: Set a correct ipv4 address
|
||||||
|
invalidMessage: Invalid ipv4 address
|
15
src/tools/ipv4-address-converter/locales/zh.yml
Normal file
15
src/tools/ipv4-address-converter/locales/zh.yml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
tools:
|
||||||
|
ipv4-address-converter:
|
||||||
|
title: IPv4 地址转换器
|
||||||
|
description: 将 IP 地址转换为十进制、二进制、十六进制,甚至 IPv6
|
||||||
|
|
||||||
|
ipv4AddressLabel: 'IPv4 地址:'
|
||||||
|
ipv4AddressPlaceholder: 'IPv4 地址...'
|
||||||
|
decimal: '十进制:'
|
||||||
|
hexadecimal: '十六进制:'
|
||||||
|
binary: '二进制:'
|
||||||
|
ipv6: 'IPv6:'
|
||||||
|
ipv6Short: 'IPv6(简写):'
|
||||||
|
|
||||||
|
errorPlaceholder: 设置一个正确的 IPv4 地址
|
||||||
|
invalidMessage: 无效的 IPv4 地址
|
|
@ -1,11 +1,11 @@
|
||||||
import { UnfoldMoreOutlined } from '@vicons/material';
|
import { UnfoldMoreOutlined } from '@vicons/material';
|
||||||
import { defineTool } from '../tool';
|
import { defineTool } from '../tool';
|
||||||
|
import { translate as t } from '@/plugins/i18n.plugin';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'IPv4 range expander',
|
name: t('tools.ipv4-range-expander.title'),
|
||||||
path: '/ipv4-range-expander',
|
path: '/ipv4-range-expander',
|
||||||
description:
|
description: t('tools.ipv4-range-expander.description'),
|
||||||
'Given a start and an end IPv4 address this tool calculates a valid IPv4 network with its CIDR notation.',
|
|
||||||
keywords: ['ipv4', 'range', 'expander', 'subnet', 'creator', 'cidr'],
|
keywords: ['ipv4', 'range', 'expander', 'subnet', 'creator', 'cidr'],
|
||||||
component: () => import('./ipv4-range-expander.vue'),
|
component: () => import('./ipv4-range-expander.vue'),
|
||||||
icon: UnfoldMoreOutlined,
|
icon: UnfoldMoreOutlined,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { calculateCidr } from './ipv4-range-expander.service';
|
||||||
import ResultRow from './result-row.vue';
|
import ResultRow from './result-row.vue';
|
||||||
import { useValidation } from '@/composable/validation';
|
import { useValidation } from '@/composable/validation';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1');
|
const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1');
|
||||||
const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255');
|
const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255');
|
||||||
|
|
||||||
|
@ -17,22 +18,22 @@ const calculatedValues: {
|
||||||
getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined
|
getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
label: 'Start address',
|
label: t('tools.ipv4-range-expander.startAddress'),
|
||||||
getOldValue: () => rawStartAddress.value,
|
getOldValue: () => rawStartAddress.value,
|
||||||
getNewValue: result => result?.newStart,
|
getNewValue: result => result?.newStart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'End address',
|
label: t('tools.ipv4-range-expander.endAddress'),
|
||||||
getOldValue: () => rawEndAddress.value,
|
getOldValue: () => rawEndAddress.value,
|
||||||
getNewValue: result => result?.newEnd,
|
getNewValue: result => result?.newEnd,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Addresses in range',
|
label: t('tools.ipv4-range-expander.addressesInRange'),
|
||||||
getOldValue: result => result?.oldSize?.toLocaleString(),
|
getOldValue: result => result?.oldSize?.toLocaleString(),
|
||||||
getNewValue: result => result?.newSize?.toLocaleString(),
|
getNewValue: result => result?.newSize?.toLocaleString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'CIDR',
|
label: t('tools.ipv4-range-expander.CIDR'),
|
||||||
getOldValue: () => '',
|
getOldValue: () => '',
|
||||||
getNewValue: result => result?.newCidr,
|
getNewValue: result => result?.newCidr,
|
||||||
},
|
},
|
||||||
|
@ -40,11 +41,11 @@ const calculatedValues: {
|
||||||
|
|
||||||
const startIpValidation = useValidation({
|
const startIpValidation = useValidation({
|
||||||
source: rawStartAddress,
|
source: rawStartAddress,
|
||||||
rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }],
|
rules: [{ message: t('tools.ipv4-range-expander.invalidMessage'), validator: ip => isValidIpv4({ ip }) }],
|
||||||
});
|
});
|
||||||
const endIpValidation = useValidation({
|
const endIpValidation = useValidation({
|
||||||
source: rawEndAddress,
|
source: rawEndAddress,
|
||||||
rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }],
|
rules: [{ message: t('tools.ipv4-range-expander.invalidMessage'), validator: ip => isValidIpv4({ ip }) }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined);
|
const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined);
|
||||||
|
@ -61,16 +62,16 @@ function onSwitchStartEndClicked() {
|
||||||
<div mb-4 flex gap-4>
|
<div mb-4 flex gap-4>
|
||||||
<c-input-text
|
<c-input-text
|
||||||
v-model:value="rawStartAddress"
|
v-model:value="rawStartAddress"
|
||||||
label="Start address"
|
:label="t('tools.ipv4-range-expander.startAddress')"
|
||||||
placeholder="Start IPv4 address..."
|
:placeholder="t('tools.ipv4-range-expander.startAddressPlaceholder')"
|
||||||
:validation="startIpValidation"
|
:validation="startIpValidation"
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<c-input-text
|
<c-input-text
|
||||||
v-model:value="rawEndAddress"
|
v-model:value="rawEndAddress"
|
||||||
label="End address"
|
:label="t('tools.ipv4-range-expander.endAddress')"
|
||||||
placeholder="End IPv4 address..."
|
:placeholder="t('tools.ipv4-range-expander.endAddressPlaceholder')"
|
||||||
:validation="endIpValidation"
|
:validation="endIpValidation"
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
|
@ -83,10 +84,10 @@ function onSwitchStartEndClicked() {
|
||||||
|
|
||||||
</th>
|
</th>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
old value
|
{{ t('tools.ipv4-range-expander.oldValue') }}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col">
|
<th scope="col">
|
||||||
new value
|
{{ t('tools.ipv4-range-expander.newValue') }}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -102,17 +103,16 @@ function onSwitchStartEndClicked() {
|
||||||
</n-table>
|
</n-table>
|
||||||
<n-alert
|
<n-alert
|
||||||
v-else-if="startIpValidation.isValid && endIpValidation.isValid"
|
v-else-if="startIpValidation.isValid && endIpValidation.isValid"
|
||||||
title="Invalid combination of start and end IPv4 address"
|
:title="t('tools.ipv4-range-expander.errorMessage')"
|
||||||
type="error"
|
type="error"
|
||||||
>
|
>
|
||||||
<div my-3 op-70>
|
<div my-3 op-70>
|
||||||
The end IPv4 address is lower than the start IPv4 address. This is not valid and no result could be calculated.
|
{{ t('tools.ipv4-range-expander.errorDesc') }}
|
||||||
In the most cases the solution to solve this problem is to change start and end address.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<c-button @click="onSwitchStartEndClicked">
|
<c-button @click="onSwitchStartEndClicked">
|
||||||
<n-icon mr-2 :component="Exchange" depth="3" size="22" />
|
<n-icon mr-2 :component="Exchange" depth="3" size="22" />
|
||||||
Switch start and end IPv4 address
|
{{ t('tools.ipv4-range-expander.switchStartEnd') }}
|
||||||
</c-button>
|
</c-button>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
18
src/tools/ipv4-range-expander/locales/en.yml
Normal file
18
src/tools/ipv4-range-expander/locales/en.yml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
tools:
|
||||||
|
ipv4-range-expander:
|
||||||
|
title: IPv4 range expander
|
||||||
|
description: Given a start and an end IPv4 address this tool calculates a valid IPv4 network with its CIDR notation.
|
||||||
|
|
||||||
|
startAddressPlaceholder: Start IPv4 address...
|
||||||
|
endAddressPlaceholder: End IPv4 address...
|
||||||
|
startAddress: Start address
|
||||||
|
endAddress: End address
|
||||||
|
addressesInRange: Addresses in range
|
||||||
|
CIDR: CIDR
|
||||||
|
oldValue: old value
|
||||||
|
newValue: new value
|
||||||
|
|
||||||
|
errorMessage: Invalid combination of start and end IPv4 address
|
||||||
|
errorDesc: The end IPv4 address is lower than the start IPv4 address. This is not valid and no result could be calculated. In the most cases the solution to solve this problem is to change start and end address.
|
||||||
|
switchStartEnd: Switch start and end IPv4 address
|
||||||
|
invalidMessage: Invalid ipv4 address
|
18
src/tools/ipv4-range-expander/locales/zh.yml
Normal file
18
src/tools/ipv4-range-expander/locales/zh.yml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
tools:
|
||||||
|
ipv4-range-expander:
|
||||||
|
title: IPv4 范围扩展器
|
||||||
|
description: 给定起始和结束 IPv4 地址,此工具计算出一个有效的 IPv4 网络及其 CIDR 表示法。
|
||||||
|
|
||||||
|
startAddressPlaceholder: 起始 IPv4 地址...
|
||||||
|
endAddressPlaceholder: 结束 IPv4 地址...
|
||||||
|
startAddress: 起始地址
|
||||||
|
endAddress: 结束地址
|
||||||
|
addressesInRange: 范围内的地址
|
||||||
|
CIDR: CIDR
|
||||||
|
oldValue: 旧值
|
||||||
|
newValue: 新值
|
||||||
|
|
||||||
|
errorMessage: 起始和结束 IPv4 地址的组合无效
|
||||||
|
errorDesc: 结束 IPv4 地址低于起始 IPv4 地址。这是无效的,无法计算结果。大多数情况下,解决此问题的方法是更改起始和结束地址。
|
||||||
|
switchStartEnd: 切换起始和结束 IPv4 地址
|
||||||
|
invalidMessage: 无效的 IPv4 地址
|
|
@ -1,10 +1,11 @@
|
||||||
import { RouterOutlined } from '@vicons/material';
|
import { RouterOutlined } from '@vicons/material';
|
||||||
import { defineTool } from '../tool';
|
import { defineTool } from '../tool';
|
||||||
|
import { translate as t } from '@/plugins/i18n.plugin';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'IPv4 subnet calculator',
|
name: t('tools.ipv4-subnet-calculator.title'),
|
||||||
path: '/ipv4-subnet-calculator',
|
path: '/ipv4-subnet-calculator',
|
||||||
description: 'Parse your IPv4 CIDR blocks and get all the info you need about your sub network.',
|
description: t('tools.ipv4-subnet-calculator.description'),
|
||||||
keywords: ['ipv4', 'subnet', 'calculator', 'mask', 'network', 'cidr', 'netmask', 'bitmask', 'broadcast', 'address'],
|
keywords: ['ipv4', 'subnet', 'calculator', 'mask', 'network', 'cidr', 'netmask', 'bitmask', 'broadcast', 'address'],
|
||||||
component: () => import('./ipv4-subnet-calculator.vue'),
|
component: () => import('./ipv4-subnet-calculator.vue'),
|
||||||
icon: RouterOutlined,
|
icon: RouterOutlined,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { withDefaultOnError } from '@/utils/defaults';
|
||||||
import { isNotThrowing } from '@/utils/boolean';
|
import { isNotThrowing } from '@/utils/boolean';
|
||||||
import SpanCopyable from '@/components/SpanCopyable.vue';
|
import SpanCopyable from '@/components/SpanCopyable.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24');
|
const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24');
|
||||||
|
|
||||||
const getNetworkInfo = (address: string) => new Netmask(address.trim());
|
const getNetworkInfo = (address: string) => new Netmask(address.trim());
|
||||||
|
@ -15,7 +16,7 @@ const networkInfo = computed(() => withDefaultOnError(() => getNetworkInfo(ip.va
|
||||||
|
|
||||||
const ipValidationRules = [
|
const ipValidationRules = [
|
||||||
{
|
{
|
||||||
message: 'We cannot parse this address, check the format',
|
message: t('tools.ipv4-subnet-calculator.invalidMessage'),
|
||||||
validator: (value: string) => isNotThrowing(() => getNetworkInfo(value.trim())),
|
validator: (value: string) => isNotThrowing(() => getNetworkInfo(value.trim())),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -26,50 +27,50 @@ const sections: {
|
||||||
undefinedFallback?: string
|
undefinedFallback?: string
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
label: 'Netmask',
|
label: t('tools.ipv4-subnet-calculator.networkMask'),
|
||||||
getValue: block => block.toString(),
|
getValue: block => block.toString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Network address',
|
label: t('tools.ipv4-subnet-calculator.networkAddress'),
|
||||||
getValue: ({ base }) => base,
|
getValue: ({ base }) => base,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Network mask',
|
label: t('tools.ipv4-subnet-calculator.networkMask'),
|
||||||
getValue: ({ mask }) => mask,
|
getValue: ({ mask }) => mask,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Network mask in binary',
|
label: t('tools.ipv4-subnet-calculator.networkMaskInBinary'),
|
||||||
getValue: ({ bitmask }) => ('1'.repeat(bitmask) + '0'.repeat(32 - bitmask)).match(/.{8}/g)?.join('.') ?? '',
|
getValue: ({ bitmask }) => ('1'.repeat(bitmask) + '0'.repeat(32 - bitmask)).match(/.{8}/g)?.join('.') ?? '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'CIDR notation',
|
label: t('tools.ipv4-subnet-calculator.CIDRNotation'),
|
||||||
getValue: ({ bitmask }) => `/${bitmask}`,
|
getValue: ({ bitmask }) => `/${bitmask}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Wildcard mask',
|
label: t('tools.ipv4-subnet-calculator.wildcardMask'),
|
||||||
getValue: ({ hostmask }) => hostmask,
|
getValue: ({ hostmask }) => hostmask,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Network size',
|
label: t('tools.ipv4-subnet-calculator.networkSize'),
|
||||||
getValue: ({ size }) => String(size),
|
getValue: ({ size }) => String(size),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'First address',
|
label: t('tools.ipv4-subnet-calculator.firstAddress'),
|
||||||
getValue: ({ first }) => first,
|
getValue: ({ first }) => first,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Last address',
|
label: t('tools.ipv4-subnet-calculator.lastAddress'),
|
||||||
getValue: ({ last }) => last,
|
getValue: ({ last }) => last,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Broadcast address',
|
label: t('tools.ipv4-subnet-calculator.broadcastAddress'),
|
||||||
getValue: ({ broadcast }) => broadcast,
|
getValue: ({ broadcast }) => broadcast,
|
||||||
undefinedFallback: 'No broadcast address with this mask',
|
undefinedFallback: t('tools.ipv4-subnet-calculator.broadcastFallback'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'IP class',
|
label: t('tools.ipv4-subnet-calculator.IPClass'),
|
||||||
getValue: ({ base: ip }) => getIPClass({ ip }),
|
getValue: ({ base: ip }) => getIPClass({ ip }),
|
||||||
undefinedFallback: 'Unknown class type',
|
undefinedFallback: t('tools.ipv4-subnet-calculator.IPClassFallback'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -86,8 +87,8 @@ function switchToBlock({ count = 1 }: { count?: number }) {
|
||||||
<div>
|
<div>
|
||||||
<c-input-text
|
<c-input-text
|
||||||
v-model:value="ip"
|
v-model:value="ip"
|
||||||
label="An IPv4 address with or without mask"
|
:label="t('tools.ipv4-subnet-calculator.ipv4AddressLabel')"
|
||||||
placeholder="The ipv4 address..."
|
:placeholder="t('tools.ipv4-subnet-calculator.ipv4AddressPlaceholder')"
|
||||||
:validation-rules="ipValidationRules"
|
:validation-rules="ipValidationRules"
|
||||||
mb-4
|
mb-4
|
||||||
/>
|
/>
|
||||||
|
@ -112,10 +113,10 @@ function switchToBlock({ count = 1 }: { count?: number }) {
|
||||||
<div mt-3 flex items-center justify-between>
|
<div mt-3 flex items-center justify-between>
|
||||||
<c-button @click="switchToBlock({ count: -1 })">
|
<c-button @click="switchToBlock({ count: -1 })">
|
||||||
<n-icon :component="ArrowLeft" />
|
<n-icon :component="ArrowLeft" />
|
||||||
Previous block
|
{{ t('tools.ipv4-subnet-calculator.previousBlock') }}
|
||||||
</c-button>
|
</c-button>
|
||||||
<c-button @click="switchToBlock({ count: 1 })">
|
<c-button @click="switchToBlock({ count: 1 })">
|
||||||
Next block
|
{{ t('tools.ipv4-subnet-calculator.nextBlock') }}
|
||||||
<n-icon :component="ArrowRight" />
|
<n-icon :component="ArrowRight" />
|
||||||
</c-button>
|
</c-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
24
src/tools/ipv4-subnet-calculator/locales/en.yml
Normal file
24
src/tools/ipv4-subnet-calculator/locales/en.yml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
tools:
|
||||||
|
ipv4-subnet-calculator:
|
||||||
|
title: IPv4 subnet calculator
|
||||||
|
description: Parse your IPv4 CIDR blocks and get all the info you need about your sub network.
|
||||||
|
|
||||||
|
ipv4AddressLabel: An IPv4 address with or without mask
|
||||||
|
ipv4AddressPlaceholder: The ipv4 address...
|
||||||
|
netmask: Netmask
|
||||||
|
networkAddress: Network address
|
||||||
|
networkMask: Network mask
|
||||||
|
networkMaskInBinary: Network mask in binary
|
||||||
|
CIDRNotation: CIDR notation
|
||||||
|
wildcardMask: Wildcard mask
|
||||||
|
networkSize: Network size
|
||||||
|
firstAddress: First address
|
||||||
|
lastAddress: Last address
|
||||||
|
broadcastAddress: Broadcast address
|
||||||
|
broadcastFallback: No broadcast address with this mask
|
||||||
|
IPClass: IP class
|
||||||
|
IPClassFallback: Unknown class type
|
||||||
|
|
||||||
|
previousBlock: Previous block
|
||||||
|
nextBlock: Next block
|
||||||
|
invalidMessage: We cannot parse this address, check the format
|
24
src/tools/ipv4-subnet-calculator/locales/zh.yml
Normal file
24
src/tools/ipv4-subnet-calculator/locales/zh.yml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
tools:
|
||||||
|
ipv4-subnet-calculator:
|
||||||
|
title: IPv4 子网计算器
|
||||||
|
description: 解析您的 IPv4 CIDR 块,并获取关于您的子网络的所有所需信息。
|
||||||
|
|
||||||
|
ipv4AddressLabel: 带有或不带掩码的 IPv4 地址
|
||||||
|
ipv4AddressPlaceholder: IPv4 地址...
|
||||||
|
netmask: 子网掩码
|
||||||
|
networkAddress: 网络地址
|
||||||
|
networkMask: 网络掩码
|
||||||
|
networkMaskInBinary: 二进制网络掩码
|
||||||
|
CIDRNotation: CIDR 表示法
|
||||||
|
wildcardMask: 通配符掩码
|
||||||
|
networkSize: 网络大小
|
||||||
|
firstAddress: 第一个地址
|
||||||
|
lastAddress: 最后一个地址
|
||||||
|
broadcastAddress: 广播地址
|
||||||
|
broadcastFallback: 此掩码无广播地址
|
||||||
|
IPClass: IP 类别
|
||||||
|
IPClassFallback: 未知类别
|
||||||
|
|
||||||
|
previousBlock: 前一个块
|
||||||
|
nextBlock: 下一个块
|
||||||
|
invalidMessage: 我们无法解析此地址,请检查格式
|
|
@ -1,10 +1,11 @@
|
||||||
import { BuildingFactory } from '@vicons/tabler';
|
import { BuildingFactory } from '@vicons/tabler';
|
||||||
import { defineTool } from '../tool';
|
import { defineTool } from '../tool';
|
||||||
|
import { translate as t } from '@/plugins/i18n.plugin';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'IPv6 ULA generator',
|
name: t('tools.ipv6-ula-generator.title'),
|
||||||
path: '/ipv6-ula-generator',
|
path: '/ipv6-ula-generator',
|
||||||
description: 'Generate your own local, non-routable IP addresses on your network according to RFC4193.',
|
description: t('tools.ipv6-ula-generator.description'),
|
||||||
keywords: ['ipv6', 'ula', 'generator', 'rfc4193', 'network', 'private'],
|
keywords: ['ipv6', 'ula', 'generator', 'rfc4193', 'network', 'private'],
|
||||||
component: () => import('./ipv6-ula-generator.vue'),
|
component: () => import('./ipv6-ula-generator.vue'),
|
||||||
icon: BuildingFactory,
|
icon: BuildingFactory,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { SHA1 } from 'crypto-js';
|
||||||
import InputCopyable from '@/components/InputCopyable.vue';
|
import InputCopyable from '@/components/InputCopyable.vue';
|
||||||
import { macAddressValidation } from '@/utils/macAddress';
|
import { macAddressValidation } from '@/utils/macAddress';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const macAddress = ref('20:37:06:12:34:56');
|
const macAddress = ref('20:37:06:12:34:56');
|
||||||
const calculatedSections = computed(() => {
|
const calculatedSections = computed(() => {
|
||||||
const timestamp = new Date().getTime();
|
const timestamp = new Date().getTime();
|
||||||
|
@ -14,15 +15,15 @@ const calculatedSections = computed(() => {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'IPv6 ULA:',
|
label: t('tools.ipv6-ula-generator.IPv6ULA'),
|
||||||
value: `${ula}::/48`,
|
value: `${ula}::/48`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'First routable block:',
|
label: t('tools.ipv6-ula-generator.firstRoutableBlock'),
|
||||||
value: `${ula}:0::/64`,
|
value: `${ula}:0::/64`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Last routable block:',
|
label: t('tools.ipv6-ula-generator.lastRoutableBlock'),
|
||||||
value: `${ula}:ffff::/64`,
|
value: `${ula}:ffff::/64`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -33,16 +34,15 @@ const addressValidation = macAddressValidation(macAddress);
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-alert title="Info" type="info">
|
<n-alert :title="t('tools.ipv6-ula-generator.info')" type="info">
|
||||||
This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed,
|
{{ t('tools.ipv6-ula-generator.infoDetail') }}
|
||||||
and the lower 40 bits to generate your random ULA.
|
|
||||||
</n-alert>
|
</n-alert>
|
||||||
|
|
||||||
<c-input-text
|
<c-input-text
|
||||||
v-model:value="macAddress"
|
v-model:value="macAddress"
|
||||||
placeholder="Type a MAC address"
|
:placeholder="t('tools.ipv6-ula-generator.macAddressPlaceholder')"
|
||||||
clearable
|
clearable
|
||||||
label="MAC address:"
|
:label="t('tools.ipv6-ula-generator.macAddressLabel')"
|
||||||
raw-text
|
raw-text
|
||||||
my-8
|
my-8
|
||||||
:validation="addressValidation"
|
:validation="addressValidation"
|
||||||
|
|
13
src/tools/ipv6-ula-generator/locales/en.yml
Normal file
13
src/tools/ipv6-ula-generator/locales/en.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
tools:
|
||||||
|
ipv6-ula-generator:
|
||||||
|
title: IPv6 ULA generator
|
||||||
|
description: Generate your own local, non-routable IP addresses on your network according to RFC4193.
|
||||||
|
|
||||||
|
macAddressLabel: 'MAC address:'
|
||||||
|
macAddressPlaceholder: 'Type a MAC address'
|
||||||
|
IPv6ULA: 'IPv6 ULA:'
|
||||||
|
firstRoutableBlock: 'First routable block:'
|
||||||
|
lastRoutableBlock: 'Last routable block:'
|
||||||
|
|
||||||
|
info: Info
|
||||||
|
infoDetail: This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed, and the lower 40 bits to generate your random ULA.
|
13
src/tools/ipv6-ula-generator/locales/zh.yml
Normal file
13
src/tools/ipv6-ula-generator/locales/zh.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
tools:
|
||||||
|
ipv6-ula-generator:
|
||||||
|
title: IPv6 ULA 生成器
|
||||||
|
description: 根据 RFC4193,在您的网络中生成您自己的本地、不可路由的 IP 地址。
|
||||||
|
|
||||||
|
macAddressLabel: 'MAC 地址:'
|
||||||
|
macAddressPlaceholder: '输入 MAC 地址'
|
||||||
|
IPv6ULA: 'IPv6 ULA:'
|
||||||
|
firstRoutableBlock: '第一个可路由的块:'
|
||||||
|
lastRoutableBlock: '最后一个可路由的块:'
|
||||||
|
|
||||||
|
info: 信息
|
||||||
|
infoDetail: 此工具使用 IETF 建议的第一种方法,使用当前时间戳加上 MAC 地址,进行 sha1 哈希处理,并使用低 40 位生成您的随机 ULA。
|
|
@ -1,10 +1,11 @@
|
||||||
import { Devices } from '@vicons/tabler';
|
import { Devices } from '@vicons/tabler';
|
||||||
import { defineTool } from '../tool';
|
import { defineTool } from '../tool';
|
||||||
|
import { translate as t } from '@/plugins/i18n.plugin';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'MAC address generator',
|
name: t('tools.mac-address-generator.title'),
|
||||||
path: '/mac-address-generator',
|
path: '/mac-address-generator',
|
||||||
description: 'Enter the quantity and prefix. MAC addresses will be generated in your chosen case (uppercase or lowercase)',
|
description: t('tools.mac-address-generator.description'),
|
||||||
keywords: ['mac', 'address', 'generator', 'random', 'prefix'],
|
keywords: ['mac', 'address', 'generator', 'random', 'prefix'],
|
||||||
component: () => import('./mac-address-generator.vue'),
|
component: () => import('./mac-address-generator.vue'),
|
||||||
icon: Devices,
|
icon: Devices,
|
||||||
|
|
17
src/tools/mac-address-generator/locales/en.yml
Normal file
17
src/tools/mac-address-generator/locales/en.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
tools:
|
||||||
|
mac-address-generator:
|
||||||
|
title: MAC address generator
|
||||||
|
description: Enter the quantity and prefix. MAC addresses will be generated in your chosen case (uppercase or lowercase)
|
||||||
|
|
||||||
|
quantity: 'Quantity:'
|
||||||
|
prefixLabel: 'MAC address prefix:'
|
||||||
|
prefixPlaceholder: 'Set a prefix, e.g. 64:16:7F'
|
||||||
|
case: 'Case:'
|
||||||
|
separator: 'Separator:'
|
||||||
|
uppercase: Uppercase
|
||||||
|
lowercase: Lowercase
|
||||||
|
none: None
|
||||||
|
|
||||||
|
refreshBtn: Refresh
|
||||||
|
copyBtn: Copy
|
||||||
|
copied: MAC addresses copied to the clipboard
|
17
src/tools/mac-address-generator/locales/zh.yml
Normal file
17
src/tools/mac-address-generator/locales/zh.yml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
tools:
|
||||||
|
mac-address-generator:
|
||||||
|
title: MAC 地址生成器
|
||||||
|
description: 输入数量和前缀。MAC 地址将以您选择的大小写形式(大写或小写)生成。
|
||||||
|
|
||||||
|
quantity: '数量:'
|
||||||
|
prefixLabel: 'MAC 地址前缀:'
|
||||||
|
prefixPlaceholder: '设置前缀,例如 64:16:7F'
|
||||||
|
case: '大小写:'
|
||||||
|
separator: '分隔符:'
|
||||||
|
uppercase: 大写
|
||||||
|
lowercase: 小写
|
||||||
|
none: 无
|
||||||
|
|
||||||
|
refreshBtn: 刷新
|
||||||
|
copyBtn: 复制
|
||||||
|
copied: MAC 地址已复制到剪贴板
|
|
@ -5,14 +5,15 @@ import { computedRefreshable } from '@/composable/computedRefreshable';
|
||||||
import { useCopy } from '@/composable/copy';
|
import { useCopy } from '@/composable/copy';
|
||||||
import { usePartialMacAddressValidation } from '@/utils/macAddress';
|
import { usePartialMacAddressValidation } from '@/utils/macAddress';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const amount = useStorage('mac-address-generator-amount', 1);
|
const amount = useStorage('mac-address-generator-amount', 1);
|
||||||
const macAddressPrefix = useStorage('mac-address-generator-prefix', '64:16:7F');
|
const macAddressPrefix = useStorage('mac-address-generator-prefix', '64:16:7F');
|
||||||
|
|
||||||
const prefixValidation = usePartialMacAddressValidation(macAddressPrefix);
|
const prefixValidation = usePartialMacAddressValidation(macAddressPrefix);
|
||||||
|
|
||||||
const casesTransformers = [
|
const casesTransformers = [
|
||||||
{ label: 'Uppercase', value: (value: string) => value.toUpperCase() },
|
{ label: t('tools.mac-address-generator.uppercase'), value: (value: string) => value.toUpperCase() },
|
||||||
{ label: 'Lowercase', value: (value: string) => value.toLowerCase() },
|
{ label: t('tools.mac-address-generator.lowercase'), value: (value: string) => value.toLowerCase() },
|
||||||
];
|
];
|
||||||
const caseTransformer = ref(casesTransformers[0].value);
|
const caseTransformer = ref(casesTransformers[0].value);
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ const separators = [
|
||||||
value: '.',
|
value: '.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'None',
|
label: t('tools.mac-address-generator.none'),
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -48,20 +49,20 @@ const [macAddresses, refreshMacAddresses] = computedRefreshable(() => {
|
||||||
return ids.join('\n');
|
return ids.join('\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
const { copy } = useCopy({ source: macAddresses, text: 'MAC addresses copied to the clipboard' });
|
const { copy } = useCopy({ source: macAddresses, text: t('tools.mac-address-generator.copied') });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div flex flex-col justify-center gap-2>
|
<div flex flex-col justify-center gap-2>
|
||||||
<div flex items-center>
|
<div flex items-center>
|
||||||
<label w-150px pr-12px text-right> Quantity:</label>
|
<label w-150px pr-12px text-right> {{ t('tools.mac-address-generator.quantity') }}</label>
|
||||||
<n-input-number v-model:value="amount" min="1" max="100" flex-1 />
|
<n-input-number v-model:value="amount" min="1" max="100" flex-1 />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<c-input-text
|
<c-input-text
|
||||||
v-model:value="macAddressPrefix"
|
v-model:value="macAddressPrefix"
|
||||||
label="MAC address prefix:"
|
:label="t('tools.mac-address-generator.prefixLabel')"
|
||||||
placeholder="Set a prefix, e.g. 64:16:7F"
|
:placeholder="t('tools.mac-address-generator.prefixPlaceholder')"
|
||||||
clearable
|
clearable
|
||||||
label-position="left"
|
label-position="left"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
|
@ -74,7 +75,7 @@ const { copy } = useCopy({ source: macAddresses, text: 'MAC addresses copied to
|
||||||
<c-buttons-select
|
<c-buttons-select
|
||||||
v-model:value="caseTransformer"
|
v-model:value="caseTransformer"
|
||||||
:options="casesTransformers"
|
:options="casesTransformers"
|
||||||
label="Case:"
|
:label="t('tools.mac-address-generator.case')"
|
||||||
label-width="150px"
|
label-width="150px"
|
||||||
label-align="right"
|
label-align="right"
|
||||||
/>
|
/>
|
||||||
|
@ -82,7 +83,7 @@ const { copy } = useCopy({ source: macAddresses, text: 'MAC addresses copied to
|
||||||
<c-buttons-select
|
<c-buttons-select
|
||||||
v-model:value="separator"
|
v-model:value="separator"
|
||||||
:options="separators"
|
:options="separators"
|
||||||
label="Separator:"
|
:label="t('tools.mac-address-generator.separator')"
|
||||||
label-width="150px"
|
label-width="150px"
|
||||||
label-align="right"
|
label-align="right"
|
||||||
/>
|
/>
|
||||||
|
@ -93,10 +94,10 @@ const { copy } = useCopy({ source: macAddresses, text: 'MAC addresses copied to
|
||||||
|
|
||||||
<div flex justify-center gap-2>
|
<div flex justify-center gap-2>
|
||||||
<c-button data-test-id="refresh" @click="refreshMacAddresses()">
|
<c-button data-test-id="refresh" @click="refreshMacAddresses()">
|
||||||
Refresh
|
{{ t('tools.mac-address-generator.refreshBtn') }}
|
||||||
</c-button>
|
</c-button>
|
||||||
<c-button @click="copy()">
|
<c-button @click="copy()">
|
||||||
Copy
|
{{ t('tools.mac-address-generator.copyBtn') }}
|
||||||
</c-button>
|
</c-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { Devices } from '@vicons/tabler';
|
import { Devices } from '@vicons/tabler';
|
||||||
import { defineTool } from '../tool';
|
import { defineTool } from '../tool';
|
||||||
|
import { translate as t } from '@/plugins/i18n.plugin';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'MAC address lookup',
|
name: t('tools.mac-address-lookup.title'),
|
||||||
path: '/mac-address-lookup',
|
path: '/mac-address-lookup',
|
||||||
description: 'Find the vendor and manufacturer of a device by its MAC address.',
|
description: t('tools.mac-address-lookup.description'),
|
||||||
keywords: ['mac', 'address', 'lookup', 'vendor', 'parser', 'manufacturer'],
|
keywords: ['mac', 'address', 'lookup', 'vendor', 'parser', 'manufacturer'],
|
||||||
component: () => import('./mac-address-lookup.vue'),
|
component: () => import('./mac-address-lookup.vue'),
|
||||||
icon: Devices,
|
icon: Devices,
|
||||||
|
|
12
src/tools/mac-address-lookup/locales/en.yml
Normal file
12
src/tools/mac-address-lookup/locales/en.yml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
tools:
|
||||||
|
mac-address-lookup:
|
||||||
|
title: MAC address lookup
|
||||||
|
description: Find the vendor and manufacturer of a device by its MAC address.
|
||||||
|
|
||||||
|
MACAddressLabel: 'MAC address:'
|
||||||
|
MACAddressPlaceholder: Type a MAC address
|
||||||
|
vendorInfo: 'Vendor info:'
|
||||||
|
unknownAddress: Unknown vendor for this address
|
||||||
|
copyBtn: Copy vendor info
|
||||||
|
|
||||||
|
copied: Vendor info copied to the clipboard
|
12
src/tools/mac-address-lookup/locales/zh.yml
Normal file
12
src/tools/mac-address-lookup/locales/zh.yml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
tools:
|
||||||
|
mac-address-lookup:
|
||||||
|
title: MAC 地址查询
|
||||||
|
description: 通过 MAC 地址查找设备的供应商和制造商。
|
||||||
|
|
||||||
|
MACAddressLabel: 'MAC 地址:'
|
||||||
|
MACAddressPlaceholder: 输入 MAC 地址
|
||||||
|
vendorInfo: '供应商信息:'
|
||||||
|
unknownAddress: 此地址的供应商未知
|
||||||
|
copyBtn: 复制供应商信息
|
||||||
|
|
||||||
|
copied: 供应商信息已复制到剪贴板
|
|
@ -3,21 +3,22 @@ import db from 'oui-data';
|
||||||
import { macAddressValidationRules } from '@/utils/macAddress';
|
import { macAddressValidationRules } from '@/utils/macAddress';
|
||||||
import { useCopy } from '@/composable/copy';
|
import { useCopy } from '@/composable/copy';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6);
|
const getVendorValue = (address: string) => address.trim().replace(/[.:-]/g, '').toUpperCase().substring(0, 6);
|
||||||
|
|
||||||
const macAddress = ref('20:37:06:12:34:56');
|
const macAddress = ref('20:37:06:12:34:56');
|
||||||
const details = computed<string | undefined>(() => (db as Record<string, string>)[getVendorValue(macAddress.value)]);
|
const details = computed<string | undefined>(() => (db as Record<string, string>)[getVendorValue(macAddress.value)]);
|
||||||
|
|
||||||
const { copy } = useCopy({ source: () => details.value ?? '', text: 'Vendor info copied to the clipboard' });
|
const { copy } = useCopy({ source: () => details.value ?? '', text: t('tools.mac-address-lookup.copied') });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<c-input-text
|
<c-input-text
|
||||||
v-model:value="macAddress"
|
v-model:value="macAddress"
|
||||||
label="MAC address:"
|
:label="t('tools.mac-address-lookup.MACAddressLabel')"
|
||||||
size="large"
|
size="large"
|
||||||
placeholder="Type a MAC address"
|
:placeholder="t('tools.mac-address-lookup.MACAddressPlaceholder')"
|
||||||
clearable
|
clearable
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
autocorrect="off"
|
autocorrect="off"
|
||||||
|
@ -28,7 +29,7 @@ const { copy } = useCopy({ source: () => details.value ?? '', text: 'Vendor info
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div mb-5px>
|
<div mb-5px>
|
||||||
Vendor info:
|
{{ t('tools.mac-address-lookup.vendorInfo') }}
|
||||||
</div>
|
</div>
|
||||||
<c-card mb-5>
|
<c-card mb-5>
|
||||||
<div v-if="details">
|
<div v-if="details">
|
||||||
|
@ -38,13 +39,13 @@ const { copy } = useCopy({ source: () => details.value ?? '', text: 'Vendor info
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else italic op-60>
|
<div v-else italic op-60>
|
||||||
Unknown vendor for this address
|
{{ t('tools.mac-address-lookup.unknownAddress') }}
|
||||||
</div>
|
</div>
|
||||||
</c-card>
|
</c-card>
|
||||||
|
|
||||||
<div flex justify-center>
|
<div flex justify-center>
|
||||||
<c-button :disabled="!details" @click="copy()">
|
<c-button :disabled="!details" @click="copy()">
|
||||||
Copy vendor info
|
{{ t('tools.mac-address-lookup.copyBtn') }}
|
||||||
</c-button>
|
</c-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { Math } from '@vicons/tabler';
|
import { Math } from '@vicons/tabler';
|
||||||
import { defineTool } from '../tool';
|
import { defineTool } from '../tool';
|
||||||
|
import { translate as t } from '@/plugins/i18n.plugin';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'Math evaluator',
|
name: t('tools.math-evaluator.title'),
|
||||||
path: '/math-evaluator',
|
path: '/math-evaluator',
|
||||||
description: 'A calculator for evaluating mathematical expressions. You can use functions like sqrt, cos, sin, abs, etc.',
|
description: t('tools.math-evaluator.description'),
|
||||||
keywords: [
|
keywords: [
|
||||||
'math',
|
'math',
|
||||||
'evaluator',
|
'evaluator',
|
||||||
|
|
7
src/tools/math-evaluator/locales/en.yml
Normal file
7
src/tools/math-evaluator/locales/en.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
tools:
|
||||||
|
math-evaluator:
|
||||||
|
title: Math evaluator
|
||||||
|
description: A calculator for evaluating mathematical expressions. You can use functions like sqrt, cos, sin, abs, etc.
|
||||||
|
|
||||||
|
inputPlaceholder: 'Your math expression (ex: 2*sqrt(6) )...'
|
||||||
|
result: Result
|
7
src/tools/math-evaluator/locales/zh.yml
Normal file
7
src/tools/math-evaluator/locales/zh.yml
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
tools:
|
||||||
|
math-evaluator:
|
||||||
|
title: 数学表达式求值器
|
||||||
|
description: 用于计算数学表达式的计算器。您可以使用函数如sqrt,cos,sin,abs等。
|
||||||
|
|
||||||
|
inputPlaceholder: '您的数学表达式(例如:2*sqrt(6))...'
|
||||||
|
result: 结果
|
|
@ -3,6 +3,7 @@ import { evaluate } from 'mathjs';
|
||||||
|
|
||||||
import { withDefaultOnError } from '@/utils/defaults';
|
import { withDefaultOnError } from '@/utils/defaults';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const expression = ref('');
|
const expression = ref('');
|
||||||
|
|
||||||
const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', ''));
|
const result = computed(() => withDefaultOnError(() => evaluate(expression.value) ?? '', ''));
|
||||||
|
@ -14,14 +15,14 @@ const result = computed(() => withDefaultOnError(() => evaluate(expression.value
|
||||||
v-model:value="expression"
|
v-model:value="expression"
|
||||||
rows="1"
|
rows="1"
|
||||||
multiline
|
multiline
|
||||||
placeholder="Your math expression (ex: 2*sqrt(6) )..."
|
:placeholder="t('tools.math-evaluator.inputPlaceholder')"
|
||||||
raw-text
|
raw-text
|
||||||
monospace
|
monospace
|
||||||
autofocus
|
autofocus
|
||||||
autosize
|
autosize
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<c-card v-if="result !== ''" title="Result " mt-5>
|
<c-card v-if="result !== ''" :title="t('tools.math-evaluator.result')" mt-5>
|
||||||
{{ result }}
|
{{ result }}
|
||||||
</c-card>
|
</c-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { Percentage } from '@vicons/tabler';
|
import { Percentage } from '@vicons/tabler';
|
||||||
import { defineTool } from '../tool';
|
import { defineTool } from '../tool';
|
||||||
|
import { translate as t } from '@/plugins/i18n.plugin';
|
||||||
|
|
||||||
export const tool = defineTool({
|
export const tool = defineTool({
|
||||||
name: 'Percentage calculator',
|
name: t('tools.percentage-calculator.title'),
|
||||||
path: '/percentage-calculator',
|
path: '/percentage-calculator',
|
||||||
description: 'Easily calculate percentages from a value to another value, or from a percentage to a value.',
|
description: t('tools.percentage-calculator.description'),
|
||||||
keywords: ['percentage', 'calculator', 'calculate', 'value', 'number', '%'],
|
keywords: ['percentage', 'calculator', 'calculate', 'value', 'number', '%'],
|
||||||
component: () => import('./percentage-calculator.vue'),
|
component: () => import('./percentage-calculator.vue'),
|
||||||
icon: Percentage,
|
icon: Percentage,
|
||||||
|
|
13
src/tools/percentage-calculator/locales/en.yml
Normal file
13
src/tools/percentage-calculator/locales/en.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
tools:
|
||||||
|
percentage-calculator:
|
||||||
|
title: Percentage calculator
|
||||||
|
description: Easily calculate percentages from a value to another value, or from a percentage to a value.
|
||||||
|
|
||||||
|
whatIs: What is
|
||||||
|
percentageX: '% of'
|
||||||
|
XIsWhatPercentOfY: X is what percent of Y
|
||||||
|
percentOf: is what percent of
|
||||||
|
percentage: What is the percentage increase/decrease
|
||||||
|
from: From
|
||||||
|
to: To
|
||||||
|
result: Result
|
13
src/tools/percentage-calculator/locales/zh.yml
Normal file
13
src/tools/percentage-calculator/locales/zh.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
tools:
|
||||||
|
percentage-calculator:
|
||||||
|
title: 百分比计算器
|
||||||
|
description: 轻松计算从一个值到另一个值的百分比,或从一个百分比到一个值。
|
||||||
|
|
||||||
|
whatIs: 什么是
|
||||||
|
percentageX: '% 对于'
|
||||||
|
XIsWhatPercentOfY: X 是 Y 的百分之多少
|
||||||
|
percentOf: 是百分之几
|
||||||
|
percentage: 百分比的增长/减少是多少
|
||||||
|
from: 从
|
||||||
|
to: 到
|
||||||
|
result: 结果
|
|
@ -1,4 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const { t } = useI18n();
|
||||||
const percentageX = ref();
|
const percentageX = ref();
|
||||||
const percentageY = ref();
|
const percentageY = ref();
|
||||||
const percentageResult = computed(() => {
|
const percentageResult = computed(() => {
|
||||||
|
@ -34,43 +35,43 @@ const percentageIncreaseDecrease = computed(() => {
|
||||||
<div style="margin: 0 auto; max-width: 600px">
|
<div style="margin: 0 auto; max-width: 600px">
|
||||||
<c-card mb-3>
|
<c-card mb-3>
|
||||||
<div mb-3 sm:hidden>
|
<div mb-3 sm:hidden>
|
||||||
What is
|
{{ t('tools.percentage-calculator.whatIs') }}
|
||||||
</div>
|
</div>
|
||||||
<div flex gap-2>
|
<div flex gap-2>
|
||||||
<div hidden pt-1 sm:block style="min-width: 48px;">
|
<div hidden pt-1 sm:block style="min-width: 48px;">
|
||||||
What is
|
{{ t('tools.percentage-calculator.whatIs') }}
|
||||||
</div>
|
</div>
|
||||||
<n-input-number v-model:value="percentageX" data-test-id="percentageX" placeholder="X" />
|
<n-input-number v-model:value="percentageX" data-test-id="percentageX" placeholder="X" />
|
||||||
<div min-w-fit pt-1>
|
<div min-w-fit pt-1>
|
||||||
% of
|
{{ t('tools.percentage-calculator.percentageX') }}
|
||||||
</div>
|
</div>
|
||||||
<n-input-number v-model:value="percentageY" data-test-id="percentageY" placeholder="Y" />
|
<n-input-number v-model:value="percentageY" data-test-id="percentageY" placeholder="Y" />
|
||||||
<input-copyable v-model:value="percentageResult" data-test-id="percentageResult" readonly placeholder="Result" style="max-width: 150px;" />
|
<input-copyable v-model:value="percentageResult" data-test-id="percentageResult" readonly :placeholder="t('tools.percentage-calculator.result')" style="max-width: 150px;" />
|
||||||
</div>
|
</div>
|
||||||
</c-card>
|
</c-card>
|
||||||
|
|
||||||
<c-card mb-3>
|
<c-card mb-3>
|
||||||
<div mb-3 sm:hidden>
|
<div mb-3 sm:hidden>
|
||||||
X is what percent of Y
|
{{ t('tools.percentage-calculator.XIsWhatPercentOfY') }}
|
||||||
</div>
|
</div>
|
||||||
<div flex gap-2>
|
<div flex gap-2>
|
||||||
<n-input-number v-model:value="numberX" data-test-id="numberX" placeholder="X" />
|
<n-input-number v-model:value="numberX" data-test-id="numberX" placeholder="X" />
|
||||||
<div hidden min-w-fit pt-1 sm:block>
|
<div hidden min-w-fit pt-1 sm:block>
|
||||||
is what percent of
|
{{ t('tools.percentage-calculator.percentOf') }}
|
||||||
</div>
|
</div>
|
||||||
<n-input-number v-model:value="numberY" data-test-id="numberY" placeholder="Y" />
|
<n-input-number v-model:value="numberY" data-test-id="numberY" placeholder="Y" />
|
||||||
<input-copyable v-model:value="numberResult" data-test-id="numberResult" readonly placeholder="Result" style="max-width: 150px;" />
|
<input-copyable v-model:value="numberResult" data-test-id="numberResult" readonly :placeholder="t('tools.percentage-calculator.result')" style="max-width: 150px;" />
|
||||||
</div>
|
</div>
|
||||||
</c-card>
|
</c-card>
|
||||||
|
|
||||||
<c-card mb-3>
|
<c-card mb-3>
|
||||||
<div mb-3>
|
<div mb-3>
|
||||||
What is the percentage increase/decrease
|
{{ t('tools.percentage-calculator.percentage') }}
|
||||||
</div>
|
</div>
|
||||||
<div flex gap-2>
|
<div flex gap-2>
|
||||||
<n-input-number v-model:value="numberFrom" data-test-id="numberFrom" placeholder="From" />
|
<n-input-number v-model:value="numberFrom" data-test-id="numberFrom" :placeholder="t('tools.percentage-calculator.from')" />
|
||||||
<n-input-number v-model:value="numberTo" data-test-id="numberTo" placeholder="To" />
|
<n-input-number v-model:value="numberTo" data-test-id="numberTo" :placeholder="t('tools.percentage-calculator.to')" />
|
||||||
<input-copyable v-model:value="percentageIncreaseDecrease" data-test-id="percentageIncreaseDecrease" readonly placeholder="Result" style="max-width: 150px;" />
|
<input-copyable v-model:value="percentageIncreaseDecrease" data-test-id="percentageIncreaseDecrease" readonly :placeholder="t('tools.percentage-calculator.result')" style="max-width: 150px;" />
|
||||||
</div>
|
</div>
|
||||||
</c-card>
|
</c-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue