mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-24 22:39:14 +01:00
add label generation api (#498)
* add label generation api * show location name on labels * add label scan page * dispose of code reader when navigating away from scan page * save label to png * implement code suggestions * fix label padding and margin * update swagger docs * add print from browser dialog Co-authored-by: fidoriel <49869342+fidoriel@users.noreply.github.com> * increase label description font weight * update documentation label file suffix * fix scanner components import * fix linting issues --------- Co-authored-by: fidoriel <49869342+fidoriel@users.noreply.github.com>
This commit is contained in:
125
frontend/components/global/LabelMaker.vue
Normal file
125
frontend/components/global/LabelMaker.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { route } from "../../lib/api/base";
|
||||
import MdiPrinterPos from "~icons/mdi/printer-pos";
|
||||
import MdiFileDownload from "~icons/mdi/file-download";
|
||||
|
||||
const props = defineProps<{
|
||||
type: string;
|
||||
id: string;
|
||||
}>();
|
||||
|
||||
const pubApi = usePublicApi();
|
||||
const toast = useNotifier();
|
||||
|
||||
const { data: status } = useAsyncData(async () => {
|
||||
const { data, error } = await pubApi.status();
|
||||
if (error) {
|
||||
toast.error("Failed to load status");
|
||||
return;
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
const printModal = ref(false);
|
||||
const serverPrinting = ref(false);
|
||||
|
||||
function openPrint() {
|
||||
printModal.value = true;
|
||||
}
|
||||
|
||||
function browserPrint() {
|
||||
const printWindow = window.open(getLabelUrl(false), "popup=true");
|
||||
|
||||
if (printWindow !== null) {
|
||||
printWindow.onload = () => {
|
||||
printWindow.print();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function serverPrint() {
|
||||
serverPrinting.value = true;
|
||||
try {
|
||||
await fetch(getLabelUrl(true));
|
||||
} catch (err) {
|
||||
console.error("Failed to print labels:", err);
|
||||
serverPrinting.value = false;
|
||||
toast.error("Failed to print label");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success("Label printed");
|
||||
printModal.value = false;
|
||||
serverPrinting.value = false;
|
||||
}
|
||||
|
||||
function downloadLabel() {
|
||||
const link = document.createElement("a");
|
||||
link.download = `label-${props.id}.png`;
|
||||
link.href = getLabelUrl(false);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function getLabelUrl(print: boolean): string {
|
||||
const params = { print };
|
||||
|
||||
if (props.type === "item") {
|
||||
return route(`/labelmaker/item/${props.id}`, params);
|
||||
} else if (props.type === "location") {
|
||||
return route(`/labelmaker/location/${props.id}`, params);
|
||||
} else if (props.type === "asset") {
|
||||
return route(`/labelmaker/asset/${props.id}`, params);
|
||||
} else {
|
||||
throw new Error(`Unexpected labelmaker type ${props.type}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<BaseModal v-model="printModal">
|
||||
<template #title>{{ $t("components.global.label_maker.print") }}</template>
|
||||
<p>
|
||||
{{ $t("components.global.label_maker.confirm_description") }}
|
||||
</p>
|
||||
<img :src="getLabelUrl(false)" />
|
||||
<div class="modal-action">
|
||||
<BaseButton
|
||||
v-if="status?.labelPrinting || false"
|
||||
type="submit"
|
||||
:loading="serverPrinting"
|
||||
@click="serverPrint"
|
||||
>{{ $t("components.global.label_maker.server_print") }}</BaseButton
|
||||
>
|
||||
<BaseButton type="submit" @click="browserPrint">{{
|
||||
$t("components.global.label_maker.browser_print")
|
||||
}}</BaseButton>
|
||||
</div>
|
||||
</BaseModal>
|
||||
|
||||
<div class="dropdown dropdown-left">
|
||||
<slot>
|
||||
<label tabindex="0" class="btn btn-sm">
|
||||
{{ $t("components.global.label_maker.titles") }}
|
||||
</label>
|
||||
</slot>
|
||||
<ul class="dropdown-content menu compact rounded-box w-52 bg-base-100 shadow-lg">
|
||||
<li>
|
||||
<button @click="openPrint">
|
||||
<MdiPrinterPos name="mdi-printer-pos" class="mr-2" />
|
||||
{{ $t("components.global.label_maker.print") }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button @click="downloadLabel">
|
||||
<MdiFileDownload name="mdi-file-download" class="mr-2" />
|
||||
{{ $t("components.global.label_maker.download") }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -130,6 +130,7 @@
|
||||
import MdiHome from "~icons/mdi/home";
|
||||
import MdiFileTree from "~icons/mdi/file-tree";
|
||||
import MdiMagnify from "~icons/mdi/magnify";
|
||||
import MdiQrcodeScan from "~icons/mdi/qrcode-scan";
|
||||
import MdiAccount from "~icons/mdi/account";
|
||||
import MdiCog from "~icons/mdi/cog";
|
||||
import MdiWrench from "~icons/mdi/wrench";
|
||||
@@ -270,6 +271,13 @@
|
||||
name: computed(() => t("menu.search")),
|
||||
to: "/items",
|
||||
},
|
||||
{
|
||||
icon: MdiQrcodeScan,
|
||||
id: 3,
|
||||
active: computed(() => route.path === "/scanner"),
|
||||
name: computed(() => t("menu.scanner")),
|
||||
to: "/scanner",
|
||||
},
|
||||
{
|
||||
icon: MdiWrench,
|
||||
id: 3,
|
||||
|
||||
@@ -409,6 +409,7 @@ export interface APISummary {
|
||||
build: Build;
|
||||
demo: boolean;
|
||||
health: boolean;
|
||||
labelPrinting: boolean;
|
||||
latest: Latest;
|
||||
message: string;
|
||||
title: string;
|
||||
|
||||
@@ -51,6 +51,14 @@
|
||||
},
|
||||
"password_score": {
|
||||
"password_strength": "Password Strength"
|
||||
},
|
||||
"label_maker": {
|
||||
"titles": "Labels",
|
||||
"server_print": "Print on Server",
|
||||
"browser_print": "Print from Browser",
|
||||
"print": "Print label",
|
||||
"download": "Download Label",
|
||||
"confirm_description": "Are you sure you want to print this label?"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
@@ -298,7 +306,8 @@
|
||||
"maintenance": "Maintenance",
|
||||
"profile": "Profile",
|
||||
"search": "Search",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"scanner": "Scanner"
|
||||
},
|
||||
"profile": {
|
||||
"active": "Active",
|
||||
@@ -366,5 +375,12 @@
|
||||
"bill_of_materials_sub": "Generates a CSV (Comma Separated Values) file that can be imported into a spreadsheet program. This is a summary of your inventory with basic item and pricing information."
|
||||
},
|
||||
"reports_sub": "Generate different reports for your inventory."
|
||||
},
|
||||
"scanner": {
|
||||
"select_video_source": "Pick a video source",
|
||||
"unsupported": "Media Stream API is not supported",
|
||||
"error": "An error occurred while scanning",
|
||||
"no_sources": "No video sources available",
|
||||
"invalid_url": "Invalid barcode URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@vueuse/nuxt": "^10.11.1",
|
||||
"@vueuse/router": "^10.11.1",
|
||||
"@zxing/library": "^0.21.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
@@ -519,6 +519,9 @@
|
||||
{{ $t(t.name) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<LabelMaker v-if="typeof item.assetId === 'string' && item.assetId != ''" :id="item.assetId" type="asset" />
|
||||
<LabelMaker v-else :id="item.id" type="item" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@
|
||||
{{ $t("global.edit") }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<LabelMaker :id="location.id" type="location" />
|
||||
<BaseButton class="btn btn-sm" @click="confirmDelete()">
|
||||
<MdiDelete name="mdi-delete" class="mr-2" />
|
||||
{{ $t("global.delete") }}
|
||||
|
||||
120
frontend/pages/scanner.vue
Normal file
120
frontend/pages/scanner.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { BrowserMultiFormatReader, NotFoundException } from "@zxing/library";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
definePageMeta({
|
||||
middleware: ["auth"],
|
||||
});
|
||||
useHead({
|
||||
title: "Homebox | Scanner",
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const sources = ref<MediaDeviceInfo[]>([]);
|
||||
const selectedSource = ref<string | null>(null);
|
||||
const loading = ref(false);
|
||||
const video = ref<HTMLVideoElement>();
|
||||
const codeReader = new BrowserMultiFormatReader();
|
||||
const errorMessage = ref<string | null>(null);
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
console.error("Scanner error:", error);
|
||||
errorMessage.value = t("scanner.error");
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (!(navigator && navigator.mediaDevices && "enumerateDevices" in navigator.mediaDevices)) {
|
||||
errorMessage.value = t("scanner.unsupported");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const devices = await codeReader.listVideoInputDevices();
|
||||
sources.value = devices;
|
||||
|
||||
if (devices.length > 0) {
|
||||
selectedSource.value = devices[0].deviceId;
|
||||
} else {
|
||||
errorMessage.value = t("scanner.no_sources");
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
});
|
||||
|
||||
// stop the code reader when navigating away
|
||||
onBeforeUnmount(() => codeReader.reset());
|
||||
|
||||
watch(selectedSource, async newSource => {
|
||||
codeReader.reset();
|
||||
|
||||
try {
|
||||
await codeReader.decodeFromVideoDevice(newSource, video.value!, (result, err) => {
|
||||
if (result && !loading.value) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const url = new URL(result.getText());
|
||||
if (!url.pathname.startsWith("/")) {
|
||||
throw new Error(t("scanner.invalid_url"));
|
||||
}
|
||||
const sanitizedPath = url.pathname.replace(/[^a-zA-Z0-9-_/]/g, "");
|
||||
navigateTo(sanitizedPath);
|
||||
} catch (err) {
|
||||
loading.value = false;
|
||||
handleError(err);
|
||||
}
|
||||
}
|
||||
if (err && !(err instanceof NotFoundException)) {
|
||||
console.error(err);
|
||||
handleError(err);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-12 pb-16">
|
||||
<section>
|
||||
<div class="mx-auto">
|
||||
<div class="max-w-screen-md">
|
||||
<div v-if="errorMessage" role="alert" class="alert alert-error mb-5 shadow-lg">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="size-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm">{{ errorMessage }}</span>
|
||||
</div>
|
||||
<video ref="video" class="rounded-box shadow-lg" poster="data:image/gif,AAAA"></video>
|
||||
<select v-model="selectedSource" class="select mt-4 w-full shadow-lg">
|
||||
<option disabled selected :value="null">{{ t("scanner.select_video_source") }}</option>
|
||||
<option v-for="source in sources" :key="source.deviceId" :value="source.deviceId">
|
||||
{{ source.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
video {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
43
frontend/pnpm-lock.yaml
generated
43
frontend/pnpm-lock.yaml
generated
@@ -44,6 +44,9 @@ importers:
|
||||
'@vueuse/router':
|
||||
specifier: ^10.11.1
|
||||
version: 10.11.1(vue-router@4.5.0(vue@3.4.8(typescript@5.6.2)))(vue@3.4.8(typescript@5.6.2))
|
||||
'@zxing/library':
|
||||
specifier: ^0.21.3
|
||||
version: 0.21.3
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.20(postcss@8.4.49)
|
||||
@@ -2120,6 +2123,13 @@ packages:
|
||||
'@vueuse/shared@12.5.0':
|
||||
resolution: {integrity: sha512-vMpcL1lStUU6O+kdj6YdHDixh0odjPAUM15uJ9f7MY781jcYkIwFA4iv2EfoIPO6vBmvutI1HxxAwmf0cx5ISQ==}
|
||||
|
||||
'@zxing/library@0.21.3':
|
||||
resolution: {integrity: sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==}
|
||||
engines: {node: '>= 10.4.0'}
|
||||
|
||||
'@zxing/text-encoding@0.9.0':
|
||||
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
|
||||
|
||||
abbrev@2.0.0:
|
||||
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||
@@ -5332,6 +5342,10 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: '>=4.2.0'
|
||||
|
||||
ts-custom-error@3.3.1:
|
||||
resolution: {integrity: sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
@@ -7467,7 +7481,7 @@ snapshots:
|
||||
|
||||
'@nuxtjs/eslint-config-typescript@12.1.0(eslint@8.57.1)(typescript@5.6.2)':
|
||||
dependencies:
|
||||
'@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
|
||||
'@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.2)
|
||||
eslint: 8.57.1
|
||||
@@ -7480,10 +7494,10 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)':
|
||||
'@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)':
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
|
||||
eslint-plugin-n: 15.7.0(eslint@8.57.1)
|
||||
eslint-plugin-node: 11.1.0(eslint@8.57.1)
|
||||
@@ -8363,6 +8377,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@zxing/library@0.21.3':
|
||||
dependencies:
|
||||
ts-custom-error: 3.3.1
|
||||
optionalDependencies:
|
||||
'@zxing/text-encoding': 0.9.0
|
||||
|
||||
'@zxing/text-encoding@0.9.0':
|
||||
optional: true
|
||||
|
||||
abbrev@2.0.0: {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
@@ -9332,7 +9355,7 @@ snapshots:
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
|
||||
eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
eslint: 8.57.1
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
|
||||
@@ -9363,7 +9386,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1):
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
@@ -9397,7 +9420,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@8.57.1)
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -10772,7 +10795,7 @@ snapshots:
|
||||
unenv: 1.10.0
|
||||
unimport: 3.14.5(rollup@4.29.1)
|
||||
unplugin: 1.16.0
|
||||
unplugin-vue-router: 0.10.9(rollup@4.29.1)(vue-router@4.5.0(vue@3.4.8(typescript@5.6.2)))(vue@3.5.13(typescript@5.6.2))
|
||||
unplugin-vue-router: 0.10.9(rollup@4.29.1)(vue-router@4.5.0(vue@3.5.13(typescript@5.6.2)))(vue@3.5.13(typescript@5.6.2))
|
||||
unstorage: 1.14.4(db0@0.2.1)(ioredis@5.4.2)
|
||||
untyped: 1.5.2
|
||||
vue: 3.5.13(typescript@5.6.2)
|
||||
@@ -12040,6 +12063,8 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.6.2
|
||||
|
||||
ts-custom-error@3.3.1: {}
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tsconfig-paths@3.15.0:
|
||||
@@ -12204,7 +12229,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-vue-router@0.10.9(rollup@4.29.1)(vue-router@4.5.0(vue@3.4.8(typescript@5.6.2)))(vue@3.5.13(typescript@5.6.2)):
|
||||
unplugin-vue-router@0.10.9(rollup@4.29.1)(vue-router@4.5.0(vue@3.5.13(typescript@5.6.2)))(vue@3.5.13(typescript@5.6.2)):
|
||||
dependencies:
|
||||
'@babel/types': 7.26.3
|
||||
'@rollup/pluginutils': 5.1.4(rollup@4.29.1)
|
||||
@@ -12221,7 +12246,7 @@ snapshots:
|
||||
unplugin: 2.0.0-beta.1
|
||||
yaml: 2.7.0
|
||||
optionalDependencies:
|
||||
vue-router: 4.5.0(vue@3.4.8(typescript@5.6.2))
|
||||
vue-router: 4.5.0(vue@3.5.13(typescript@5.6.2))
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- vue
|
||||
|
||||
Reference in New Issue
Block a user