feat: Implement WebcamFileSource for life webcam capture #12
8 changed files with 322 additions and 105 deletions
76
frontend/src/components/CameraFileSource.vue
Normal file
76
frontend/src/components/CameraFileSource.vue
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<div class="d-inline-block" v-if="show_camera">
|
||||||
|
<label for="pictures">
|
||||||
|
<slot></slot>
|
||||||
|
</label>
|
||||||
|
<input type="file" accept="image/*" multiple capture="camera" id="pictures" @change="loadFiles" class="d-none">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "CameraFileSource",
|
||||||
|
components: {
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
emits: ["input"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show_camera: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadFiles() {
|
||||||
|
const files = document.getElementById("pictures").files;
|
||||||
|
const jobs = [...files].map((file) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const buffer = reader.result;
|
||||||
|
if (!(buffer instanceof ArrayBuffer)) {
|
||||||
|
console.log(buffer)
|
||||||
|
reject("Not an ArrayBuffer");
|
||||||
|
}
|
||||||
|
const data = new Uint8Array(buffer);
|
||||||
|
const hash = nacl.crypto_hash(data).reduce((a, b) => a + b.toString(16).padStart(2, "0"), "");
|
||||||
|
var base64 = btoa(
|
||||||
|
data.reduce((a, b) => a + String.fromCharCode(b), '')
|
||||||
|
);
|
||||||
|
resolve({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
mime_type: file.type,
|
||||||
|
data: base64,
|
||||||
|
hash: hash,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.onerror = (e) => {
|
||||||
|
reject(e);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
Promise.all(jobs).then((files) => {
|
||||||
|
this.$emit("input", files)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
//detect media api
|
||||||
|
if ('capture' in document.createElement('input')) {
|
||||||
|
console.log('capture supported.');
|
||||||
|
this.show_camera = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.getUserMedia) {
|
||||||
|
console.log('getUserMedia supported.');
|
||||||
|
this.show_camera = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,33 +1,33 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="input-group">
|
<drag-drop-file-source @input="addFiles">
|
||||||
|
<h3 v-if="create">Create New</h3>
|
||||||
|
<h3 v-else>Update</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="file in files" :key="file.id">
|
<li v-for="file in whithout_images(files)" :key="file.id">
|
||||||
|
{{ file.name }}
|
||||||
|
</li>
|
||||||
|
<li v-for="file in whithout_images(item_files)" :key="file.id">
|
||||||
{{ file.name }}
|
{{ file.name }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<hr>
|
||||||
<img v-for="file in files" :key="file.id" :src="file.name" :alt="file.name" class="img-thumbnail">
|
<div>
|
||||||
</ul>
|
<img v-for="file in only_images(files)" :key="file.id" :src="file.name" :alt="file.name"
|
||||||
<h4>Files</h4>
|
class="img-thumbnail" :title="file.mime_type">
|
||||||
<div class="input-group">
|
<img v-for="file in whithout_images(item_files)" :key="file.id" :alt="file.name"
|
||||||
<input type="file" class="form-control" multiple id="files">
|
:src="'data:' + file.mime_type + ';base64,' + file.data" class="img-thumbnail border-info">
|
||||||
<button class="btn btn-outline-secondary" type="button">
|
<fs-file-source @input="addFiles">
|
||||||
<b-icon-trash></b-icon-trash>
|
<div class="img-thumbnail btn btn-outline-primary">
|
||||||
</button>
|
<b-icon-upload></b-icon-upload>
|
||||||
<button class="btn btn-outline-secondary" type="button" @click="uploadFiles">
|
</div>
|
||||||
<b-icon-upload></b-icon-upload>
|
</fs-file-source>
|
||||||
</button>
|
<camera-file-source @input="addFiles">
|
||||||
|
<div class="img-thumbnail btn btn-outline-primary">
|
||||||
|
<b-icon-camera></b-icon-camera>
|
||||||
|
</div>
|
||||||
|
</camera-file-source>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group" v-if="show_camera">
|
</drag-drop-file-source>
|
||||||
<input type="file" class="form-control" accept="image/*" multiple capture="camera" id="photos">
|
|
||||||
<button class="btn btn-outline-secondary" type="button">
|
|
||||||
<b-icon-trash></b-icon-trash>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" type="button" @click="uploadPhoto">
|
|
||||||
<b-icon-upload></b-icon-upload>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -36,108 +36,82 @@
|
||||||
height: 54px;
|
height: 54px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.img-thumbnail svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as BIcons from "bootstrap-icons-vue";
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
import {mapActions, mapState} from "vuex";
|
import {mapActions, mapState} from "vuex";
|
||||||
|
import DragDropFileSource from "@/components/DragDropFileSource.vue";
|
||||||
|
import FsFileSource from "@/components/FsFileSource.vue";
|
||||||
|
import CameraFileSource from "@/components/CameraFileSource.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CombinedFileField",
|
name: "CombinedFileField",
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
show_camera: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
...BIcons
|
...BIcons,
|
||||||
|
DragDropFileSource,
|
||||||
|
CameraFileSource,
|
||||||
|
FsFileSource
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
item_files: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
item_id: {
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
model: {
|
emits: ["change"],
|
||||||
prop: "value",
|
|
||||||
event: "input"
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(["files"]),
|
...mapState(["files"]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(["fetchFiles", "pushFile"]),
|
...mapActions(["fetchFiles", "pushFile"]),
|
||||||
async uploadFiles() {
|
async uploadFiles(files) {
|
||||||
const files = document.getElementById("files").files;
|
const jobs = files.map(async file => {
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
var reader = new FileReader();
|
|
||||||
const file = files[i];
|
|
||||||
const buffer = await new Promise((resolve, reject) => {
|
|
||||||
reader.onload = (e) => {
|
|
||||||
resolve(e.target.result);
|
|
||||||
};
|
|
||||||
reader.onerror = (e) => {
|
|
||||||
reject(e);
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(file)
|
|
||||||
});
|
|
||||||
const data = new Uint8Array(buffer);
|
|
||||||
const hash = nacl.crypto_hash(data);
|
|
||||||
var base64 = btoa(
|
|
||||||
data.reduce((a, b) => a + String.fromCharCode(b), '')
|
|
||||||
);
|
|
||||||
await this.pushFile({
|
await this.pushFile({
|
||||||
file: {
|
file: file,
|
||||||
mime_type: file.type,
|
item_id: this.item_id
|
||||||
data: base64,
|
|
||||||
},
|
|
||||||
item_id: 1
|
|
||||||
});
|
});
|
||||||
await this.fetchFiles();
|
});
|
||||||
}
|
const responses = await Promise.all(jobs);
|
||||||
|
console.log(responses);
|
||||||
|
return responses;
|
||||||
},
|
},
|
||||||
async uploadPhoto() {
|
addFiles(files) {
|
||||||
const files = document.getElementById("photos").files;
|
const newfiles = files.filter(file => !this.item_files.find(f => f.hash === file.hash));
|
||||||
for (let i = 0; i < files.length; i++) {
|
if (newfiles.length === 0) {
|
||||||
var reader = new FileReader();
|
console.log("no new files");
|
||||||
const file = files[i];
|
return;
|
||||||
const buffer = await new Promise((resolve, reject) => {
|
|
||||||
reader.onload = (e) => {
|
|
||||||
resolve(e.target.result);
|
|
||||||
};
|
|
||||||
reader.onerror = (e) => {
|
|
||||||
reject(e);
|
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(file)
|
|
||||||
});
|
|
||||||
const data = new Uint8Array(buffer);
|
|
||||||
const hash = nacl.crypto_hash(data);
|
|
||||||
var base64 = btoa(
|
|
||||||
data.reduce((a, b) => a + String.fromCharCode(b), '')
|
|
||||||
);
|
|
||||||
await this.pushFile({
|
|
||||||
file: {
|
|
||||||
mime_type: file.type,
|
|
||||||
data: base64,
|
|
||||||
},
|
|
||||||
item_id: 1
|
|
||||||
});
|
|
||||||
await this.fetchFiles();
|
|
||||||
}
|
}
|
||||||
|
if (!this.create) {
|
||||||
|
this.uploadFiles(newfiles).then((uploaded) => {
|
||||||
|
console.log(uploaded);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.$emit("change", [...this.item_files, ...newfiles]);
|
||||||
|
console.log(this.item_files);
|
||||||
|
|
||||||
|
},
|
||||||
|
only_images(files) {
|
||||||
|
return files.filter(file => file.mime_type.startsWith("image/"));
|
||||||
|
},
|
||||||
|
whithout_images(files) {
|
||||||
|
return files.filter(file => !file.mime_type.startsWith("image/"));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchFiles();
|
this.fetchFiles();
|
||||||
//detect media api
|
|
||||||
if ('capture' in document.createElement('input')) {
|
|
||||||
console.log('capture supported.');
|
|
||||||
this.show_camera = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (navigator.getUserMedia) {
|
|
||||||
console.log('getUserMedia supported.');
|
|
||||||
this.show_camera = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
88
frontend/src/components/DragDropFileSource.vue
Normal file
88
frontend/src/components/DragDropFileSource.vue
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<template>
|
||||||
|
<div @dragenter.prevent="dragenter" @dragover.prevent="dragover" @dragleave.prevent="dragleave"
|
||||||
|
@drop.prevent="drop" ref="dropzone">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dragover {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DragDropFileSource",
|
||||||
|
components: {
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
emits: ["input"],
|
||||||
|
methods: {
|
||||||
|
dragenter(e) {
|
||||||
|
if (e.dataTransfer.types.includes("Files")) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
this.$refs.dropzone.classList.add("dragover")
|
||||||
|
}else{
|
||||||
|
console.log(e.dataTransfer.types) //TODO text/uri-list?
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dragover(e) {
|
||||||
|
if (e.dataTransfer.types.includes("Files")) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
this.$refs.dropzone.classList.add("dragover")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dragleave(e) {
|
||||||
|
if (e.dataTransfer.types.includes("Files")) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
this.$refs.dropzone.classList.remove("dragover")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drop(e) {
|
||||||
|
if (e.dataTransfer.types.includes("Files")) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
this.$refs.dropzone.classList.remove("dragover")
|
||||||
|
const jobs = [...e.dataTransfer.files].map((file) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const buffer = reader.result;
|
||||||
|
if (!(buffer instanceof ArrayBuffer)) {
|
||||||
|
console.log(buffer)
|
||||||
|
reject("Not an ArrayBuffer");
|
||||||
|
}
|
||||||
|
const data = new Uint8Array(buffer);
|
||||||
|
const hash = nacl.crypto_hash(data).reduce((a, b) => a + b.toString(16).padStart(2, "0"), "");
|
||||||
|
var base64 = btoa(
|
||||||
|
data.reduce((a, b) => a + String.fromCharCode(b), '')
|
||||||
|
);
|
||||||
|
resolve({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
mime_type: file.type,
|
||||||
|
data: base64,
|
||||||
|
hash: hash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
reader.onerror = (e) => {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Promise.all(jobs).then((files) => {
|
||||||
|
this.$emit("input", files)
|
||||||
|
console.log("drop", files)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
59
frontend/src/components/FsFileSource.vue
Normal file
59
frontend/src/components/FsFileSource.vue
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<div class="d-inline-block">
|
||||||
|
<label for="files">
|
||||||
|
<slot></slot>
|
||||||
|
</label>
|
||||||
|
<input type="file" multiple id="files" @change="loadFiles" class="d-none">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as BIcons from "bootstrap-icons-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "FsFileSource",
|
||||||
|
components: {
|
||||||
|
...BIcons
|
||||||
|
},
|
||||||
|
emits: ["input"],
|
||||||
|
methods: {
|
||||||
|
loadFiles() {
|
||||||
|
const files = document.getElementById("files").files;
|
||||||
|
const jobs = [...files].map((file) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const buffer = reader.result;
|
||||||
|
if (!(buffer instanceof ArrayBuffer)) {
|
||||||
|
console.log(buffer)
|
||||||
|
reject("Not an ArrayBuffer");
|
||||||
|
}
|
||||||
|
const data = new Uint8Array(buffer);
|
||||||
|
const hash = nacl.crypto_hash(data).reduce((a, b) => a + b.toString(16).padStart(2, "0"), "");
|
||||||
|
var base64 = btoa(
|
||||||
|
data.reduce((a, b) => a + String.fromCharCode(b), '')
|
||||||
|
);
|
||||||
|
resolve({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
mime_type: file.type,
|
||||||
|
data: base64,
|
||||||
|
hash: hash,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.onerror = (e) => {
|
||||||
|
reject(e);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
Promise.all(jobs).then((files) => {
|
||||||
|
this.$emit("input", files)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -253,7 +253,7 @@ export default createStore({
|
||||||
async pushFile({state, dispatch, getters}, {item_id, file}) {
|
async pushFile({state, dispatch, getters}, {item_id, file}) {
|
||||||
const servers = await dispatch('getHomeServers')
|
const servers = await dispatch('getHomeServers')
|
||||||
const data = await servers.post(getters.signAuth, '/api/item_files/'+item_id+'/', file)
|
const data = await servers.post(getters.signAuth, '/api/item_files/'+item_id+'/', file)
|
||||||
if (data.mime_type) {
|
if (data.hash) {
|
||||||
state.files.push(data)
|
state.files.push(data)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,10 +36,10 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<a class="btn btn-primary" :href="'/inventory/' + id + '/edit'">
|
<button class="btn btn-primary" @click="$router.push('/inventory/' + id + '/edit')">
|
||||||
<b-icon-pencil-square></b-icon-pencil-square>
|
<b-icon-pencil-square></b-icon-pencil-square>
|
||||||
Edit
|
Edit
|
||||||
</a>
|
</button>
|
||||||
<button type="submit" class="btn btn-danger"
|
<button type="submit" class="btn btn-danger"
|
||||||
@click="deleteInventoryItem(item).then(() => $router.push('/inventory'))">
|
@click="deleteInventoryItem(item).then(() => $router.push('/inventory'))">
|
||||||
<b-icon-trash></b-icon-trash>
|
<b-icon-trash></b-icon-trash>
|
||||||
|
|
|
@ -6,6 +6,13 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Edit Item</div>
|
<div class="card-header">Edit Item</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<ul>
|
||||||
|
<li v-for="file in item.files" :key="file.name">
|
||||||
|
{{ file.name }} ({{ file.hash }})
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Name</label>
|
<label for="name" class="form-label">Name</label>
|
||||||
<input type="text" class="form-control" id="name" name="name"
|
<input type="text" class="form-control" id="name" name="name"
|
||||||
|
@ -31,7 +38,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="image" class="form-label">Image</label>
|
<label for="image" class="form-label">Image</label>
|
||||||
<combined-file-field :value="item.files"></combined-file-field>
|
<combined-file-field :item_files="item.files" :item_id="item.id" @change="addFiles"></combined-file-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -78,12 +85,16 @@ export default {
|
||||||
description: "",
|
description: "",
|
||||||
owned_quantity: 0,
|
owned_quantity: 0,
|
||||||
image: "",
|
image: "",
|
||||||
|
files: [],
|
||||||
...this.inventory_items.find(item => item.id === parseInt(this.id))
|
...this.inventory_items.find(item => item.id === parseInt(this.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(["fetchInventoryItems", "updateInventoryItem"])
|
...mapActions(["fetchInventoryItems", "updateInventoryItem"]),
|
||||||
|
addFiles(files) {
|
||||||
|
this.inventory_items.find(item => item.id === parseInt(this.id)).files = files
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.fetchInventoryItems()
|
await this.fetchInventoryItems()
|
||||||
|
|
|
@ -6,6 +6,13 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Create New Item</div>
|
<div class="card-header">Create New Item</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<ul>
|
||||||
|
<li v-for="file in item.files" :key="file.name">
|
||||||
|
{{ file.name }} ({{ file.hash }})
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Name</label>
|
<label for="name" class="form-label">Name</label>
|
||||||
<input type="text" class="form-control" id="name" name="name"
|
<input type="text" class="form-control" id="name" name="name"
|
||||||
|
@ -31,7 +38,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="image" class="form-label">Image</label>
|
<label for="image" class="form-label">Image</label>
|
||||||
<combined-file-field :value="item.files"></combined-file-field>
|
<combined-file-field :item_files="item.files" create
|
||||||
|
@change="item.files = $event"></combined-file-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
@ -71,7 +79,8 @@ export default {
|
||||||
owned_quantity: 0,
|
owned_quantity: 0,
|
||||||
image: "",
|
image: "",
|
||||||
tags: [],
|
tags: [],
|
||||||
properties: []
|
properties: [],
|
||||||
|
files: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue