Initial commit

This commit is contained in:
Viet An
2026-03-02 09:45:33 +07:00
commit d17a9e2588
415 changed files with 92113 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
<template>
<div>
<div><video ref="video" id="video" width="640" height="480" autoplay></video></div>
<div class="mt-2">
<button class="button is-primary" id="snap" v-on:click="capture()">Chụp ảnh</button>
<a class="ml-6" @click="switchView()">
<SvgIcon v-bind="{name: 'camera_switch.svg', type: 'black', size: 40}"></SvgIcon>
</a>
</div>
<canvas v-show="false" ref="canvas" id="canvas" width="640" height="480"></canvas>
</div>
</template>
<script>
export default {
data: function() {
return {
video: {},
canvas: {},
current: 'front'
}
},
mounted: function() {
this.openCamera()
},
beforeDestroy() {
var vidTrack = this.video.srcObject.getVideoTracks()
vidTrack.forEach(track => {
track.stop()
track.enabled = false
})
},
methods: {
openCamera() {
let f = this.current==='front'? {facingMode: "user"} : {facingMode: {exact: "environment"}}
this.video = this.$refs.video;
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({ video: f, audio: false }).then(stream => {
video.srcObject = stream;
this.video.play();
});
}
},
capture() {
this.canvas = this.$refs.canvas;
let scale = 600 / this.video.videoWidth;
let w = this.video.videoWidth * scale;
let h = this.video.videoHeight * scale;
this.canvas.width = w;
this.canvas.height = h;
var context = this.canvas
.getContext("2d")
.drawImage(this.video,0,0,w,h);
this.canvas.toBlob(blod=>this.saveAs(blod))
},
async saveAs(blod) {
var form = new FormData();
let name = `${this.$id()}.png`
this.fileName = `${this.$dayjs(new Date()).format("YYYYMMDDhhmmss")}-${name}`
form.append('filename', this.fileName)
form.append('name', name)
form.append('file', blod)
form.append('type', 'image')
form.append('size', 100)
form.append('user', this.$store.state.login.id)
let result = await this.$insertapi('upload', form)
if(result==='error') return
let row = result.rows[0]
const file = new File([blod], name, {type: "image/png"})
row.source = {file: file}
this.$emit('modalevent', {name: 'selectimage', data: row})
this.$emit('close')
},
switchView() {
this.current = this.current==='front'? 'back' : 'front'
var vidTrack = this.video.srcObject.getVideoTracks()
vidTrack.forEach(track => {
track.stop()
track.enabled = false
})
this.openCamera()
}
}
}
</script>

View File

@@ -0,0 +1,114 @@
<template>
<div class="card image-card m-0" style="
outline: rgba(0, 0, 0, 0.2) solid 1px;
box-shadow: none;
height: 100%;
overflow: hidden;
border-radius: 6px;
">
<div class="is-clickable" :title="file.file__name">
<div v-if="file.file__name.endsWith('.pdf')" class="p-2 is-flex is-justify-content-center is-align-items-center" style="min-height: 120px">
<SvgIcon v-bind="{ name: 'pdf.svg', type: 'primary', size: 32 }" />
</div>
<div
v-else
class="card-image"
@click="open()"
>
<figure class="image is-4by3 has-background-black">
<nuxt-img
:src="image"
loading="lazy"
alt="Hình ảnh"
style="object-fit: contain;"
/>
</figure>
</div>
</div>
<div class="card-content p-2">
<p class="is-size-7" style="overflow: hidden; white-space: pre; text-overflow: ellipsis">{{ file.file__name }}</p>
<div id="ignore" class="buttons are-small is-gap-0.5 is-justify-content-end">
<button class="button is-white" @click="this.$copyToClipboard(`${$getpath()}static/files/${file.file__file}`)">
<span class="icon">
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 18 }" />
</span>
</button>
<button class="button is-white" @click="download()">
<span class="icon">
<SvgIcon v-bind="{ name: 'download.svg', type: 'primary', size: 18 }" />
</span>
</button>
<button v-if="$getEditRights()" class="button is-white" @click="askConfirm()">
<span class="icon">
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'primary', size: 18 }" />
</span>
</button>
</div>
</div>
<Modal
v-if="showmodal"
v-bind="showmodal"
@confirm="remove()"
@remove="remove"
@close="showmodal=undefined"
/>
</div>
</template>
<script>
export default {
props: ['row', 'api', 'pagename', 'file', 'image', 'show', 'extend'],
data() {
return {
showmodal: undefined,
display: this.show || []
}
},
methods: {
open() {
if (!this.file || this.extend === false) return;
this.showmodal = {
title: this.file.file__name,
component: 'media/ChipImage',
vbind: {
extend: false,
show: this.show,
file: this.file,
image: `${this.$getpath()}download/?name=${this.file.file__file || this.file.file}&type=file`
}
}
},
info() {
if(!this.file) return;
this.showmodal = {
vbind: {file: this.file},
component: 'media/ImageAttr',
title: 'Thông tin trích xuất từ hình ảnh',
width: '35%',
height: '50vh'
}
},
download() {
let name = this.file? this.file.file__name || this.file.name : 'download'
let path = `${this.$getpath()}download/?name=${this.file.file__file || this.file.file}&type=file`
this.$download(path, name)
},
askConfirm() {
this.showmodal = {
component: 'dialog/Confirm',
title: 'Xác nhận',
width: '500px',
height: '100px',
vbind: {
content: 'Bạn có đồng ý xóa hình ảnh này không?',
duration: 10
}
}
},
remove() {
this.showmodal = undefined;
this.$emit('modalevent', {name: 'remove'});
this.$emit('remove');
},
}
}
</script>

View File

@@ -0,0 +1,91 @@
<template>
<div class="tile is-ancestor py-5 px-3 mx-0" v-if="image">
<div class="tile is-1"/>
<div class="tile is-7">
<Cropper
ref="cropper"
:src="image"
@change="onChange"
:stencil-props="getRatio" />
</div>
<div class="tile is-1"> </div>
<div class="tile">
<div v-if="avatar!==true">
<p class="mt-2 fs-16">Chọn tỷ lệ hoặc nhập chiều rộng cao</p>
<div class="tags are-medium mt-2">
<a :class="curRatio.k===v.k? 'tag is-primary' : 'tag'" v-for="(v,i) in ratios" :key="i" @click="curRatio=v">{{v.k}}</a>
</div>
<div class="block mt-5">
<b-radio v-model="radio"
native-value="replace">
Ghi đè
</b-radio>
<b-radio v-model="radio" class="ml-3"
native-value="new">
Tạo file mới
</b-radio>
</div>
<p class="mt-4">
<a class="button is-primary mr-4 mb-2" :class="loading? 'is-loading' : ''" @click="updateImage()">Lưu lại</a>
</p>
<p class="mt-2" v-if="coordinates">
Hình ảnh cắt, {{'W: ' + coordinates.width + ', H: ' + coordinates.height + ', W/H: ' + coordinates.ratio}}
</p>
</div>
<div class="is-italic has-text-grey" v-else>* Di chuyển khung để chọn hình ảnh phù hợp</div>
</div>
</div>
</template>
<script>
import { CircleStencil, Cropper } from 'vue-advanced-cropper'
import 'vue-advanced-cropper/dist/style.css'
export default {
components: {
Cropper,
CircleStencil
},
props: ['selected', 'image', 'avatar'],
data() {
return {
coordinates: undefined,
ratios: [{k: '1/1'}, {k: '5/4'}, {k:'4/3'}, {k: '3/2'}, {k: '5/3'}, {k:'16/9'}, {k: '2/1'}, {k: '3/1'}, {k: '4/5'}, {k: '3/4'}, {k: '2/3'}, {k: '3/5'}, {k: '9/16'}, {k: '1/2'}, {k: '1/3'}],
curRatio: {k: '1/1'},
radio: this.avatar? 'new' : 'replace',
rectangle: true,
loading: false
}
},
computed: {
getRatio() {
return {aspectRatio: this.$calc(this.curRatio.k)}
}
},
methods: {
onChange({ coordinates, canvas}) {
this.coordinates = coordinates
this.coordinates.ratio = (this.coordinates.width*1.00 / this.coordinates.height).toFixed(2)
},
updateImage() {
const { canvas } = this.$refs.cropper.getResult()
if (canvas) canvas.toBlob(blod=>this.saveAs(blod))
},
async saveAs(blod) {
this.loading = true
var form = new FormData();
let name = this.selected.file.indexOf('-')>0? this.selected.file.substring(15, this.selected.file.length) : this.selected.file
this.fileName = this.$dayjs(new Date()).format("YYYYMMDDhhmmss") + '-' + name
if(this.radio==='replace') this.fileName = this.selected.file
form.append('filename', this.fileName)
form.append('name', name)
form.append('file', blod)
form.append('type', 'file')
form.append('size', this.selected.size)
form.append('user', this.$store.state.login.id)
let result = await this.$insertapi('upload', form)
this.loading = false
if(result==='error') return
this.$emit('modalevent', {name: 'image', data: result.rows[0]})
}
}
}
</script>

View File

@@ -0,0 +1,59 @@
<script setup>
const { dealer } = useStore();
const { $buildFileUrl, $copyToClipboard, $getEditRights } = useNuxtApp();
const props = defineProps({
className: String,
image: Object,
editImage: Function,
downloadImage: Function,
openDeleteImageConfirm: Function
})
const url = $buildFileUrl(props.image.file__file);
</script>
<template>
<div :class="['buttons is-gap-1', className]">
<button
class="button is-small is-white"
@click="$copyToClipboard(url)"
title="Sao chép link"
>
<span class="icon">
<SvgIcon v-bind="{ name: 'copy.svg', type: 'primary', size: 18 }" />
</span>
</button>
<button
v-if="!dealer && $getEditRights()"
class="button is-small is-white"
@click="editImage(image)"
title="Sửa"
>
<span class="icon">
<SvgIcon v-bind="{ name: 'edit.svg', type: 'primary', size: 18 }" />
</span>
</button>
<button
class="button is-small is-white"
@click.prevent="downloadImage(image)"
title="Tải xuống"
>
<span class="icon">
<SvgIcon v-bind="{ name: 'download.svg', type: 'success', size: 18 }" />
</span>
</button>
<button
v-if="!dealer && $getEditRights()"
class="button is-small is-white"
@click="openDeleteImageConfirm(image)"
title="Xóa"
>
<span class="icon">
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'primary', size: 18 }" />
</span>
</button>
</div>
</template>
<style>
</style>

View File

@@ -0,0 +1,32 @@
<template>
<div>
<span class="tooltip">
<span class="dot-twitter" @click="doClick()">{{ row[attr] || 0 }}</span>
<span class="tooliptext">Hồ </span>
</span>
</div>
</template>
<script>
export default {
props: ['row', 'api', 'pagename', 'attr'],
methods: {
async doClick() {
let obj
if(this.attr==='image_count') {
let rs = await this.$getdata(this.api, {ref: this.row.id, file__type: 2})
if(rs.length>0) {
obj = {component: 'media/ImageShow', title: 'Hồ sơ', width: '80%', height: '70vh',
vbind: {row: this.row, image: rs.map(v=>v.file__file), pagename: this.pagename, api: this.api}}
} else {
obj = {component: 'media/ImageGallery', vbind: {row: this.row, pagename: this.pagename, api: this.api},
width: '50%', height: '500px', title: 'Hình ảnh'}
}
} else {
let vbind = {row: this.row, api: this.api, setting: 'media-fields', filter: {ref: this.row.id, file__type: 1}}
obj = {component: 'media/FileList', title: 'Hồ sơ', width: '50%', height: '300px', vbind: vbind}
}
this.$emit('open', {name: 'dataevent', data: {modal: obj}})
}
}
}
</script>

View File

@@ -0,0 +1,49 @@
<template>
<div v-if="files">
<div class="has-text-right" v-if="!hideopt && $getEditRights()">
<FileUpload v-bind="{ type: ['file'], position }" @files="getFiles"></FileUpload>
</div>
<div>
<div class="mt-3 has-text-grey fs-15" v-if="files.length===0 && info!==false">
Chưa tài liệu được tải lên
</div>
<DataView v-bind="vbind" v-else-if="vbind"></DataView>
</div>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
</div>
</template>
<script>
export default {
props: ['row', 'pagename', 'api', 'hideopt', 'setting', 'info', 'position'],
data() {
return {
showmodal: undefined,
files: undefined,
vbind: undefined
}
},
async created() {
this.files = await this.$getdata(this.api, {ref: this.row.id, file__type: 1})
if(this.files.length>0) this.vbind = {api: this.api, setting: this.setting || 'media-fields', data: this.files}
},
methods: {
open() {
this.showmodal = {component: 'media/Imagebox', width: '70%', title: 'Chọn hình ảnh', height: '500px'}
},
async remove(v, i) {
this.$delete(this.files, i)
await this.$deleteapi(this.api, v.id)
},
async getFiles(files) {
let arr = files.map(v=>{return {ref: this.row.id, file: v.id}})
let found = this.$findapi(this.api)
let rs = await this.$insertapi(this.api, arr, found.params.values)
if(rs!=='error') {
this.files = this.files.concat(rs)
this.vbind = undefined
setTimeout(()=>this.vbind = {api: this.api, setting: 'media-fields', data: this.files}, 100)
}
}
}
}
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div v-if="record">
<div class="field">
<label class="label">Tên<span class="has-text-danger"> * </span> </label>
<p class="control">
<input class="input" type="text" placeholder="Nhập số" v-model="record.name">
</p>
</div>
<div class="field mt-5">
<label class="label">Ghi chú<span class="has-text-danger"> * </span> </label>
<p class="control">
<textarea class="textarea" placeholder="" rows="3" v-model="record.caption"></textarea>
</p>
</div>
<div class="mt-5">
<button class="button is-primary" @click="save()">Lưu lại</button>
</div>
</div>
</template>
<script>
export default {
props: ['row', 'pagename'],
data() {
return {
record: undefined
}
},
async created() {
this.record = await this.$getdata('file', {id: this.row.file || this.row.id}, undefined, true)
},
methods: {
async save() {
let rs = await this.$updateapi('file', this.record)
}
}
}
</script>

View File

@@ -0,0 +1,47 @@
<template>
<div>
<FileUpload :type="['file']" @files="getFiles"></FileUpload>
<DataView v-if="vbind" v-bind="vbind"></DataView>
</div>
</template>
<script>
export default {
props: ['row', 'pagename', 'api', 'setting'],
data() {
return {
vbind: undefined,
files: undefined,
pagename1: this.$findpage()
}
},
async created() {
this.pagedata = this.$getpage()
this.files = await this.$getdata(this.api, {ref: this.row.id, file__type: 1})
this.vbind = {pagename: this.pagename1, api: this.api, setting: this.setting, data: this.$copy(this.files)}
},
beforeDestroy() {
this.$clearpage(this.pagename1)
},
computed: {
pagedata: {
get: function() {return this.$store.state[this.pagename1]},
set: function(val) {this.$store.commit('updateStore', {name: this.pagename1, data: val})}
}
},
methods: {
async getFiles(files) {
let arr = files.map(v=>{return {ref: this.row.id, file: v.id}})
let found = this.$findapi(this.api)
let rs = await this.$insertapi(this.api, arr, found.params.values)
if(rs==='error') return
this.files = this.files.concat(rs)
this.$store.commit('updateState', {name: this.pagename1, key: 'update', data: {data: this.$copy(this.files)}})
if(this.pagename) {
let vapi = this.api.replace('file', '')
let ele = await this.$getdata(vapi, {id: this.row.id}, undefined, true)
this.$updatepage(this.pagename, ele)
}
}
}
}
</script>

View File

@@ -0,0 +1,42 @@
<template>
<p class="py-1 border-bottom" v-for="(v,i) in vfiles">
<a class="mr-4" @click="open(v)">{{ v.name }}</a>
<a class="mr-4" @click="download(v, i)">
<SvgIcon v-bind="{name: 'download1.svg', type: 'dark', size: 16}"></SvgIcon>
</a>
<a @click="remove(v, i)" v-if="show? show.delete : false">
<SvgIcon v-bind="{name: 'bin1.svg', type: 'dark', size: 16}"></SvgIcon>
</a>
</p>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal"></Modal>
</template>
<script setup>
const { $copy, $getpath, $download } = useNuxtApp()
const emit = defineEmits(['remove', 'close'])
var props = defineProps({
files: Object,
show: Object
})
var showmodal = ref()
var vfiles = ref($copy(props.files))
function remove(v, i) {
vfiles.value.splice(i, 1)
emit('remove', {v: v, i: i})
emit('modalevent', {name: 'removefile', data: {v: v, i: i}})
if(vfiles.value.length===0) emit('close')
}
function open(v) {
if(v.name.indexOf('.png')>=0 || v.name.indexOf('.jpg')>=0 || v.name.indexOf('.jpeg')>=0) {
showmodal.value = {title: v.file__file || v.file, component: 'media/ChipImage',
vbind: {extend: false, file: v, image: `${$getpath()}static/files/${v.file__file || v.file}`}}
return
}
window.open(`${$getpath()}static/files/${v.file__file || v.file}`)
}
function download(v) {
window.open(`${$getpath()}static/files/${v.file}`, v.name)
}
watch(() => props.files, (newVal, oldVal) => {
vfiles.value = props.files
})
</script>

View File

@@ -0,0 +1,126 @@
<template>
<div>
<div class="file is-primary" id="ignore" v-if="position === 'left'">
<label class="file-label">
<input
class="file-input"
type="file"
:id="docid"
multiple
name="resume"
@change="doChange"
/>
<span class="file-cta px-2">
<span class="icon-text is-clickable">
<SvgIcon
v-bind="{ name: 'attach-file.svg', type: 'white', size: 22 }"
></SvgIcon>
<!--<span class="has-text-white">File</span>-->
</span>
</span>
</label>
</div>
<div class="field is-grouped is-grouped-right" id="ignore" v-else>
<div class="control">
<div class="file is-primary">
<label class="file-label">
<input
class="file-input"
type="file"
:id="docid"
multiple
name="resume"
@change="doChange"
/>
<span class="file-cta is-primary px-1">
<span class="icon-text is-clickable">
<SvgIcon
v-bind="{ name: 'attach-file.svg', type: 'white', size: 22 }"
></SvgIcon>
</span>
</span>
</label>
</div>
</div>
</div>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
@files="getFiles"
></Modal>
</div>
</template>
<script>
export default {
props: ["type", "position", "convert", "quality"],
data() {
return {
loading: false,
files: undefined,
dataFiles: [],
vtype: this.type || ["image"],
showmodal: undefined,
docid: this.$id(),
};
},
methods: {
getFileExtension(fileName) {
if (!fileName || typeof fileName !== "string") return "";
const parts = fileName.split(".");
return parts.length > 1 ? parts.pop().toLowerCase() : "";
},
getType(ext) {
// copied from 01-common.js
const imageFormat = ["png", "jpg", "jpeg", "bmp", "gif", "svg", "webp"];
const videoFormat = ["wmv", "avi", "mp4", "flv", "mov", "mpg", "amv", "rm"];
if (ext === 'pdf') return 'pdf';
if (imageFormat.includes(ext)) return 'image';
if (videoFormat.includes(ext)) return 'video';
return 'file';
},
doChange() {
this.dataFiles = [];
const fileList = document.getElementById(this.docid).files;
this.files = Array.from(fileList);
if (this.files.length === 0) return;
// Xác định giá trị convert: "1" nếu convert được bật, "0" nếu không
const convertValue = this.convert ? "1" : "0";
const qualityValue = this.convert && this.quality ? this.quality : null;
this.files.map((v) => {
const ext = this.getFileExtension(v.name);
const type = this.getType(ext);
if (!this.vtype.includes(type)) {
this.$snackbar(`Định dạng file "${ext}" không hợp lệ`);
return;
}
let file = this.$upload(v, type, 1, convertValue, qualityValue);
this.dataFiles.push(file);
});
this.showmodal = {
component: "media/UploadProgress",
title: "Upload files",
width: "700px",
height: "200px",
vbind: { files: this.dataFiles },
};
this.clearFileList();
},
clearFileList() {
const fileInput = document.getElementById(this.docid);
const dt = new DataTransfer();
fileInput.files = dt.files;
},
getFiles(files) {
this.$emit("files", files);
setTimeout(() => (this.showmodal = undefined), 3000);
},
},
};
</script>

View File

@@ -0,0 +1,28 @@
<template>
<div>
<iframe
:src="`https://docs.google.com/gview?url=${url}&embedded=true`"
:style="`width:${width}; height:${height};overflow: hidden;`"
frameborder="0"
></iframe>
</div>
</template>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
url: {
type: String,
required: true,
},
width: {
type: String,
default: "100%",
},
height: {
type: String,
default: "100%",
},
});
</script>

View File

@@ -0,0 +1,41 @@
<template>
<div>
<table class="table" v-if="attrs">
<thead>
<tr>
<th width="170px">Thông tin</th>
<th>Diễn giải</th>
</tr>
</thead>
<tbody>
<tr v-for="v in attrs">
<td>{{ v.name }}</td>
<td>
<span class="icon-text">
<span>{{ v.value }}</span>
<span class="ml-5" v-if="!$empty(v.match)">
<SvgIcon v-bind="{name: v.match? 'check2.svg' : 'error.svg', type: v.match? 'blue' : 'danger', size: 26}"></SvgIcon>
</span>
<span class="ml-5" v-if="!$empty(v.auth)">
<SvgIcon v-bind="{name: v.auth? 'check2.svg' : 'error.svg', type: v.auth? 'blue' : 'danger', size: 26}"></SvgIcon>
</span>
<span class="ml-5" v-if="!$empty(v.expiry)">
<SvgIcon v-bind="{name: v.expiry? 'error.svg' : 'check2.svg', type: v.expiry? 'danger' : 'blue', size: 26}"></SvgIcon>
</span>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
props: ['file'],
data() {
return {
attrs: this.file? this.file.file__verified_info || this.file.verified_info: undefined
}
}
}
</script>

View File

@@ -0,0 +1,296 @@
<script setup>
import FileActions from '@/components/media/FileActions.vue';
const props = defineProps({
image: Object,
index: Number,
layoutMode: String,
loadImages: Function,
deleteImage: Function,
viewImage: Function,
})
const {
$buildFileUrl,
$formatFileSize,
$getpath,
$patchapi,
$snackbar,
} = useNuxtApp();
const url = $buildFileUrl(props.image.file__file);
// State cho editing image
const editingImage = ref(null);
const editingImageName = ref("");
const editingImageCaption = ref("");
const editingImageHashtag = ref("");
const editingImageFileInput = ref(null);
const pendingDeleteImage = ref(null);
function editImage(image) {
editingImage.value = image;
editingImageName.value = image.file__name || "";
editingImageCaption.value = image.file__caption || "";
editingImageHashtag.value = image.file__hashtag || "";
}
async function saveEditImage() {
try {
await $patchapi("file", {
id: editingImage.value.file,
name: editingImageName.value,
caption: editingImageCaption.value?.trim() || null,
hashtag: editingImageHashtag.value?.trim() || null,
});
props.loadImages();
$snackbar("Đã cập nhật ảnh thành công", "Thành công", "Success");
} catch (error) {
console.error("Error updating image", error);
$snackbar("Cập nhật ảnh thất bại", "Lỗi", "Error");
}
resetEditRefs();
}
function resetEditRefs() {
editingImage.value = null;
editingImageName.value = "";
editingImageCaption.value = "";
editingImageHashtag.value = "";
if (editingImageFileInput.value) {
editingImageFileInput.value.value = "";
}
}
async function downloadImage(image) {
const downloadUrl = `${$getpath()}download/?name=${encodeURIComponent(image.file__file)}&type=file`;
window.open(downloadUrl);
}
async function openDeleteImageConfirm(image) {
pendingDeleteImage.value = image;
const confirmed = window.confirm(`Bạn có chắc chắn muốn xóa ảnh "${image.file__name}"?`);
if (confirmed) {
await props.deleteImage(image);
}
pendingDeleteImage.value = null;
}
</script>
<template>
<div v-if="layoutMode === 'grid'" class="column is-one-third is-one-quarter-widescreen p-2">
<div
class="card image-card"
style="
outline: 1px solid hsl(0 0% 0% / 0.2);
box-shadow: none;
height: 100%;
overflow: hidden;
">
<!-- Edit mode -->
<div
v-if="editingImage?.id === image.id"
class="card-content p-2"
>
<div class="field">
<label class="label fs-14">Tên ảnh</label>
<div class="control">
<input
class="input is-small"
type="text"
v-model="editingImageName"
>
</input>
</div>
</div>
<div class="field">
<label class="label fs-14"> tả</label>
<div class="control">
<textarea
class="textarea is-small"
v-model="editingImageCaption"
rows="3"
></textarea>
</div>
</div>
<div class="field">
<label class="label fs-14">Hashtag</label>
<div class="control">
<input
class="input is-small"
type="text"
v-model="editingImageHashtag"
>
</input>
</div>
</div>
<div class="buttons is-small mt-2">
<button
class="button is-small has-text-white is-primary"
@click="saveEditImage"
>
<span>Lưu</span>
</button>
<button
class="button is-small is-danger has-text-white"
@click="resetEditRefs"
>
<span>Hủy</span>
</button>
</div>
</div>
<!-- View mode -->
<div v-else>
<div
class="card-image"
style="cursor: pointer"
@click="props.viewImage(index)"
>
<figure class="image is-4by3 has-background-black">
<nuxt-img
:src="url"
loading="lazy"
:alt="image.file__caption || image.file__name || 'Hình ảnh'"
style="object-fit: contain;"
/>
</figure>
</div>
<div class="card-content p-2">
<div class="image-meta">
<a
:href="url"
target="_blank"
class="fs-14 has-text-weight-semibold is-clipped"
>
{{ image.file__name || "Hình ảnh" }}
</a>
<p class="is-size-7 has-text-grey is-clipped">
{{ image.file__caption }}
</p>
</div>
<FileActions v-bind="{
className: 'is-right mt-2',
image,
editImage,
downloadImage,
openDeleteImageConfirm
}" />
</div>
</div>
</div>
</div>
<template v-else>
<tr v-if="editingImage?.id === image.id">
<td colspan="5">
<div class="columns is-multiline">
<div class="column is-3">
<div class="field">
<label class="label fs-14">Tên tài liệu</label>
<div class="control">
<input
class="input is-small"
type="text"
v-model="editingImageName"
/>
</div>
</div>
</div>
<div class="column is-3">
<div class="field">
<label class="label fs-14">Mô tả</label>
<div class="control">
<input
class="input is-small"
type="text"
v-model="editingImageCaption"
/>
</div>
</div>
</div>
<div class="column is-2">
<div class="field">
<label class="label fs-14">Hashtag</label>
<div class="control">
<input
class="input is-small"
type="text"
v-model="editingImageHashtag"
/>
</div>
</div>
</div>
<div class="column">
<div class="field">
<label class="label">&nbsp;</label>
<div class="control">
<div class="buttons are-small">
<button
class="button is-primary"
@click="saveEditImage"
>
Lưu
</button>
<button
class="button is-danger"
@click="resetEditRefs"
>
Hủy
</button>
</div>
</div>
</div>
</div>
</div>
</td>
</tr>
<tr v-else>
<td>
<a
class="has-text-weight-semibold has-text-primary"
@click="props.viewImage(index)"
>
{{ image.file__name }}
</a>
<p class="is-size-7 has-text-grey">
{{ $formatFileSize(image.file__size) }}
</p>
</td>
<td>
<span class="is-size-7">{{
image.file__caption
}}</span>
</td>
<td>
<span class="is-size-7">{{
image.file__hashtag
}}</span>
</td>
<td>
<p class="is-size-7 has-text-weight-semibold">
{{ image.file__user__fullname }}
</p>
<p class="is-size-7 has-text-grey">
{{
image.create_time
? new Date(image.create_time).toLocaleString(
"vi-VN"
)
: "-"
}}
</p>
</td>
<td>
<FileActions v-bind="{
image,
editImage,
downloadImage,
openDeleteImageConfirm
}" />
</td>
</tr>
</template>
</template>

View File

@@ -0,0 +1,75 @@
<template>
<div v-if="files" class="is-fullwidth">
<div class="has-text-right mb-4" v-if="!hideopt">
<FileUpload :type="['image', 'pdf']" @files="getFiles"></FileUpload>
</div>
<div v-if="files.length > 0" class="is-flex is-gap-2 is-flex-wrap-wrap">
<ChipImage
v-for="(file, i) in files"
style="width: 160px"
@remove="remove(file, i)"
v-bind="{
row,
show,
pagename,
api,
file,
image: `${$getpath()}download/?name=${file.file__file}&type=file`,
}"
/>
</div>
<div class="mt-3 has-text-grey" v-if="files.length === 0">
{{ this.isVietnamese ? "Chưa có hình ảnh được tải lên" : "No images uploaded" }}
</div>
<Modal @close="showmodal = undefined" v-bind="showmodal" v-if="showmodal"></Modal>
</div>
</template>
<script>
import { useStore } from "@/stores/index";
export default {
setup() {
const store = useStore();
return { store };
},
props: ["row", "pagename", "api", "hideopt", "vapi", "show"],
data() {
return {
showmodal: undefined,
files: undefined,
};
},
computed: {
lang() {
return this.store.lang;
},
isVietnamese() {
return this.store.lang === "vi";
},
},
async created() {
this.files = await this.$getdata(this.api, { ref: this.row.id }); //file__type: 2
},
methods: {
async remove(v, i) {
// this.$delete(this.files, i);
this.files.splice(i, 1);
await this.$deleteapi(this.api, v.id);
},
async getFiles(files) {
let arr = files.map((v) => {
return { ref: this.row.id, file: v.id };
});
let found = this.$findapi(this.api);
let rs = await this.$insertapi(this.api, arr, found.params.values);
if (rs === "error") return;
this.files = this.files.concat(rs);
if (this.pagename) {
let vapi = this.vapi ? this.vapi : this.api.replace("file", "");
let ele = await this.$getdata(vapi, { id: this.row.id }, undefined, true);
this.$updatepage(this.pagename, ele);
}
},
},
};
</script>

View File

@@ -0,0 +1,363 @@
<template>
<div class="columns mx-0 mb-0 pb-0">
<div class="column is-6">
<div class="tags are-medium">
<a v-for="v in hashtag" @click="refreshShow(v)">
<span :class="`tag ${v.file__hashtag===current? 'is-primary' : ''}`">
{{ v.file__hashtag }}
{{ `(${v.count})` }}
</span>
</a>
</div>
</div>
<div class="column is-6">
<div class="is-flex is-align-items-center is-justify-content-flex-end">
<div class="buttons has-addons are-small my-0">
<button
v-for="layout in Object.values(layoutModes)"
:key="layout"
:class="[
'button px-5',
{ 'is-primary': layout === layoutMode },
]"
@click="layoutMode = layout"
>
<SvgIcon v-bind="{
name: `${layout}.svg`,
type: layout === layoutMode ? 'white' : 'primary',
size: 20
}" />
</button>
</div>
<div v-if="!dealer && $getEditRights('edit', { code: 'product', category: 'topmenu' })" class="ml-6 is-flex is-align-items-center">
<div class="mr-5">
<label class="checkbox mr-2">
<input type="checkbox" v-model="convertToWebp" />
<span class="ml-2">Convert to WebP</span>
</label>
<input v-if="convertToWebp"
class="input is-small"
type="number"
v-model.number="webpQuality"
min="1"
max="100"
style="width: 60px"
placeholder="80"
/>
</div>
<FileUpload
position="right"
type="image"
:convert="convertToWebp"
:quality="webpQuality"
@files="onUploaded"
/>
</div>
</div>
</div>
</div>
<component
:is="layoutMode === layoutModes.GRID ? 'div' : 'table'"
:class="layoutMode === layoutModes.GRID ? 'columns is-multiline px-2 mt-0' : 'table is-fullwidth is-bordered'"
>
<template v-if="layoutMode === layoutModes.LIST">
<thead>
<tr>
<th>File</th>
<th>Mô tả</th>
<th>Hashtag</th>
<th>Người tải lên</th>
<th>
<span class="icon-text">
<span class="mr-5">Chức năng</span>
<a class="mr-3" @click="excel()">
<SvgIcon v-bind="{ name: 'excel.png', type: 'black', size: 20 }"></SvgIcon>
</a>
<a v-if="$getEditRights()" @click="importData()">
<SvgIcon v-bind="{ name: 'upload.svg', type: 'black', size: 20 }"></SvgIcon>
</a>
</span>
</th>
</tr>
</thead>
<tbody>
<ImageCard
v-for="(image, index) in imgshow"
:key="image.id"
v-bind="{ image, index, layoutMode, loadImages, deleteImage, viewImage }"
/>
</tbody>
</template>
<ImageCard
v-else
v-for="(image, index) in imgshow"
:key="image.id"
v-bind="{ image, index, layoutMode, loadImages, deleteImage, viewImage }"
/>
</component>
<!-- Modal xem ảnh lớn -->
<div v-if="typeof activeImageIndex === 'number'" class="modal is-active">
<div class="modal-background" @click="closeViewImage"></div>
<div
class="modal-content"
style="width: 90vw; height: 90vh; border-radius: 8px; overflow: hidden"
>
<div
class="is-flex"
style="height: 100%; transition: all 0.3s ease-out"
:style="`transform: translateX(${-100 * activeImageIndex}%)`"
>
<div
v-for="image in imgshow"
:key="image.id"
class="has-background-black"
style="flex: 0 0 100%; display: flex; justify-content: center; align-items: center;"
>
<nuxt-img
loading="lazy"
:src="`${$getpath()}static/files/${image.file__file}`"
:alt="image.file__name"
style="object-fit: contain; height: 100%;"
/>
</div>
</div>
<div class="gallery-arrows">
<button
@click="showPrevImage()"
class="button is-rounded"
style="left: 24px"
>
<SvgIcon v-bind="{ name: 'left1.svg', type: 'black', size: 20 }" />
</button>
<button
@click="showNextImage()"
class="button is-rounded"
style="right: 24px"
>
<SvgIcon v-bind="{ name: 'right.svg', type: 'black', size: 20 }" />
</button>
</div>
<div class="gallery-count">
{{ activeImageIndex + 1 }}
/
{{ imgshow.length }}
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="closeViewImage"
></button>
</div>
<Modal @dataevent="dataevent" @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal" />
</template>
<script setup>
import { ref } from "vue";
import { debounce } from 'es-toolkit';
import { useNuxtApp } from "#app";
import FileUpload from "@/components/media/FileUpload.vue";
import ImageCard from '@/components/media/ImageCard.vue';
// pass only projectId for project images, pass both for product images
const props = defineProps({
productId: String,
projectId: String,
})
const { dealer } = useStore();
const {
$snackbar,
$getdata,
$insertapi,
$deleteapi,
$findapi,
$getapi,
$dayjs,
$unique
} = useNuxtApp();
const layoutModes = { GRID: 'grid', LIST: 'list' };
const layoutMode = ref(layoutModes.GRID);
const convertToWebp = ref(false);
const webpQuality = ref(80);
const project = await $getdata('project', { id: props.projectId }, undefined, true);
const images = ref([]);
const isForProduct = computed(() => props.productId !== undefined);
const product = isForProduct.value ? await $getdata("product", { id: props.productId }, undefined, true) : undefined;
const showmodal = ref()
const imgshow = ref([])
const hashtag = ref()
const current = ref()
async function loadImages() {
const values = "id,file__hashtag,file__code,file,create_time,file__code,file__type,file__doc_type,file__name,file__file,file__size,file__caption,file__user__fullname,";
const projectImages = await $getdata("projectfile", undefined, {
filter: { project: project.id, file__type: 2 },
values: values + 'project',
sort: "-create_time"
});
if (isForProduct.value) {
const productImages = await $getdata("productfile", undefined, {
filter: { product: props.productId, file__type: 2 },
values: values + 'product',
sort: "-create_time"
});
const sameTemplateImages = projectImages.filter(img => img.file__name.startsWith(product.template_name));
images.value = [...productImages, ...sameTemplateImages];
}
else {
images.value = projectImages;
}
imgshow.value = [...images.value]
hashtag.value = $unique(images.value.filter(v=>v.file__hashtag), ["file__hashtag"])
hashtag.value.map(v=>v.count = images.value.filter(x=>x.file__hashtag===v.file__hashtag).length)
}
const handleKeyDown = debounce((e) => {
if (e.key === 'Escape') closeViewImage();
if (e.key === 'ArrowLeft') showPrevImage();
if (e.key === 'ArrowRight') showNextImage();
}, 100);
onMounted(() => {
loadImages();
window.addEventListener('keydown', handleKeyDown);
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown);
})
async function attachFiles(files) {
if (!project) return 0;
const payload = files.map((file) => ({
file: file.id,
project: isForProduct.value ? undefined : project.id,
product: isForProduct.value ? product.id : undefined
}));
const result = await $insertapi(isForProduct.value ? "productfile" : "projectfile", payload);
if (result === "error") {
throw new Error("Không thể liên kết tệp");
}
return payload.length;
}
async function onUploaded(uploadedFiles) {
if (!Array.isArray(uploadedFiles) || uploadedFiles.length === 0) return;
try {
const attachedCount = await attachFiles(uploadedFiles);
if (attachedCount > 0) {
loadImages();
$snackbar(`Đã thêm ${attachedCount} ảnh thành công`, "Thành công", "Success");
}
} catch (error) {
console.error("Error attaching images:", error);
$snackbar("Không thể thêm ảnh, vui lòng thử lại", "Lỗi", "Error");
}
}
async function deleteImage(image) {
try {
await $deleteapi(isForProduct.value ? "productfile" : "projectfile", image.id);
await $deleteapi("file", image.file);
loadImages();
$snackbar("Đã xóa ảnh thành công", "Thành công", "Success");
} catch (error) {
console.error("Error deleting image:", error);
$snackbar("Xóa ảnh không thành công", "Lỗi", "Error");
}
}
async function excel() {
const found = $findapi('exportcsv')
found.params = {}
let fields = [{name: 'code', label: 'code'}, {name: 'name', label: 'name'}, {name: 'caption', label: 'caption'}, {name: 'hashtag', label: 'hashtag'}]
found.params.fields = JSON.stringify(fields)
found.url = 'exportcsv/File/'
found.params.filter = {code__in: images.value.map(v=>v.file__code)}
const rs = await $getapi([found])
if(rs === 'error') return $snackbar('Đã xảy ra lỗi. Vui lòng thử lại.')
const url = window.URL.createObjectURL(new Blob([rs[0].data]))
const link = document.createElement('a')
const fileName = `${$dayjs(new Date()).format('YYYYMMDDhhmmss')}-data.csv`
link.href = url
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
link.remove()
}
function importData() {
showmodal.value = {"title":"Nhập bút toán theo lô","vbind":{"code":"file-import"},
"width":"90%","height":"500px","component":"parameter/ImportData"}
}
function refreshShow(tag) {
if(current.value===tag.file__hashtag) {
imgshow.value = [...images.value]
current.value = null
return
}
imgshow.value = images.value.filter(v=>v.file__hashtag===tag.file__hashtag)
current.value = tag.file__hashtag
}
// State cho xem ảnh lớns
const activeImageIndex = ref(null);
function viewImage(index) {
activeImageIndex.value = index;
}
function showNextImage() {
activeImageIndex.value = (activeImageIndex.value + 1) % imgshow.value.length;
}
function showPrevImage() {
activeImageIndex.value = (activeImageIndex.value - 1 + imgshow.value.length) % imgshow.value.length;
}
function closeViewImage() {
activeImageIndex.value = null;
}
</script>
<style scoped>
.gallery-arrows {
> * {
position: absolute;
top: 50%;
background-color: rgba(255, 255, 255, 0.6);
padding: 0.5rem;
border: none;
box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.2);
&:hover {
background-color: rgba(255, 255, 255, 0.9);
}
}
}
.gallery-count {
position: absolute;
bottom: 24px;
left: 0;
right: 0;
width: fit-content;
margin-inline: auto;
padding: 4px 12px;
border-radius: 9999px;
color: white;
background-color: rgba(0, 0, 0, 0.5);
font-size: 0.85rem;
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<div class="columns mx-0" v-if="picture">
<div class="column is-narrow" style="border-right: 2px solid #D3D3D3;" v-if="viewport>1">
<div class="pr-2" style="max-height: 700px; overflow-y: auto;">
<div v-for="(v,i) in picture" class="pb-4">
<figure class="image is-clickable" style="width:80px" @click="current=i">
<img :src="v.image">
</figure>
</div>
</div>
</div>
<div class="column">
<b-carousel :autoplay="false" :indicator="viewport===1? true : false" icon-size="is-medium" v-model="current">
<b-carousel-item v-for="(item, i) in picture" :key="i">
<ChipImage @remove="check(item, i)" v-bind="{extend: false, image: item.image, file: item.file, show: ['download', 'delete']}"></ChipImage>
</b-carousel-item>
</b-carousel>
</div>
</div>
</template>
<script>
export default {
props: ['row', 'image', 'pagename', 'api'],
data() {
return {
current: 0,
picture: undefined,
timer: undefined
}
},
async created() {
let arr = []
let files = await this.$getdata('file', {file__in: this.image})
this.image.map(v=>{
let found = this.$find(files, {file: v})
arr.push({image: `${this.$getpath()}download/?name=${v}`, file: found})
})
this.picture = arr
},
computed: {
viewport: {
get: function() {return this.$store.state.viewport},
set: function(val) {this.$store.commit("updateViewPort", {viewport: val})}
}
},
methods: {
check(v, i) {
if(!this.timer) this.timer = setTimeout(()=>this.remove(v,i), 200)
},
async remove(v, i) {
this.$delete(this.picture, i)
await this.$deleteapi(this.api, v.file.id)
this.timer = undefined
if(this.picture.length===0) this.$emit('close')
}
}
}
</script>

View File

@@ -0,0 +1,303 @@
<template>
<div>
<div class="is-hidden"><img id="image" :src="image"></div>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<div class="file is-primary">
<label class="file-label">
<input class="file-input" type="file" :id="docid" multiple name="resume" @change="doChange">
<a class="button is-primary is-rounded is-small" :class="loading? 'is-loading' : null">
<SvgIcon v-bind="{name: 'add5.svg', type: 'white', size: 18}"></SvgIcon>
<span class="fs-14 ml-1">Tải lên từ máy tính</span>
</a>
</label>
</div>
</div>
<div :class="`control ${showUrl? '' : 'is-expanded'}`">
<a class="button is-dark is-rounded is-small" @click="displayInput()">
<SvgIcon v-bind="{name: 'add5.svg', type: 'white', size: 18}"></SvgIcon>
<span class="fs-14 ml-1">Tải lên từ đường dẫn</span>
</a>
</div>
<div class="control is-expanded" v-if="showUrl">
<input class="input is-small is-rounded" id="url" v-model="url" style="width: 250px;" type="text" placeholder="Nhập đường dẫn vào đây">
<a class="button is-primary is-small px-4 ml-3" @click="checkUrl()">
<SvgIcon v-bind="{name: 'upload.svg', type: 'white', size: 22}"></SvgIcon>
</a>
<a class="ml-4" @click="showUrl=false">
<SvgIcon v-bind="{name: 'close.svg', type: 'dark', size: 22}"></SvgIcon>
</a>
</div>
<div class="control">
<input class="input is-small is-rounded" v-model="search" style="width: 250px;" type="text" placeholder="Tìm kiếm" @keyup="beginSearch">
</div>
<div class="control">
<span class="is-clickable" @click="mode='image'">
<SvgIcon v-bind="{name: 'image3.svg', type: 'dark', size: 25}"></SvgIcon>
</span>
</div>
<div class="control">
<span class="is-clickable" @click="mode='list'">
<SvgIcon v-bind="{name: 'list.png', type: 'dark', size: 25}"></SvgIcon>
</span>
</div>
</div>
<DataView class="mt-3" v-bind="vbind" v-if="mode==='list'"></DataView>
<div class="tile is-ancestor mx-0 px-0 pt-3" v-else>
<div class="tile is-vertical">
<div class="tile is-parent" v-for="(v,i) in group" :key="i">
<article class="tile is-child" v-for="(k,j) in getData(i)" :key="j" @mouseover="focus=k">
<div class="image px-2 pb-2" v-if="k.file && type==='image'">
<nuxt-img :src="`${path}download?name=${k.file}`" :id="'commentImage' + k.id"></nuxt-img>
<div class="text-image" v-if="focus===k">
<a class="button is-primary is-small" @click="selectMedia(k)">
<SvgIcon v-bind="{name: 'checked.svg', type: 'white', size: 22}"></SvgIcon>
</a>
<a class="button is-primary is-small ml-2" @click="editImage(k)">
<SvgIcon v-bind="{name: 'crop.svg', type: 'white', size: 22}"></SvgIcon>
</a>
<a class="button is-primary is-small ml-2" @click="copyMedia(k, 'image')">
<SvgIcon v-bind="{name: 'copy.svg', type: 'white', size: 22}"></SvgIcon>
</a>
<a class="button is-danger is-small ml-2" @click="deleteMedia(k, 'file')">
<SvgIcon v-bind="{name: 'bin1.svg', type: 'white', size: 22}"></SvgIcon>
</a>
</div>
</div>
<div class="ml-2 mr-2" v-else-if="k.file && type==='video'">
<vue-plyr>
<video :src="path + 'static/videos/' + k.file">
<source :src="path + 'static/videos/' + k.file" type="video/mp4" size="720">
</video>
<div class="mt-2" v-if="focus===k">
<a class="button is-primary" @click="selectMedia(k)">
<SvgIcon v-bind="{name: 'check3.svg', type: 'primary', size: 22}"></SvgIcon>
</a>
</div>
</vue-plyr>
</div>
<div class="ml-2 mr-2" v-else-if="k.file && type==='file'">
{{k.file}}
<div class="mt-2" v-if="focus===k">
<a class="button is-primary" @click="selectMedia(k)">
<SvgIcon v-bind="{name: 'check3.svg', type: 'primary', size: 22}"></SvgIcon>
</a>
</div>
</div>
</article>
</div>
</div>
</div>
<Modal @close="showmodal=undefined" v-bind="showmodal" @image="updateImage" @files="getFiles" v-if="showmodal"/>
</div>
</template>
<script>
export default {
props: ['source'],
data() {
return {
file: undefined,
image: undefined,
showmodal: undefined,
data: [],
group: [],
focus: undefined,
type: 'image',
selected: undefined,
loading: false,
path: this.$getpath(),
showUrl: false,
url: undefined,
search: undefined,
mode: 'image',
timer: undefined,
vbind: {api: 'file', setting: 'image-fields'},
dataFiles: [],
files: [],
docid: this.$id()
}
},
async created() {
let found = this.$findapi('file')
found.params = {filter: {user: this.login.id}, sort: '-create_time', type__code: this.type}
let result = await this.$getapi([found])
this.data = result[0].data.rows
},
watch: {
data: function() {
this.group = []
for (let index = 0; index < this.data.length/ (this.type==='video'? 4 : 5) ; index++) {
this.group.push(index)
}
}
},
computed: {
login: {
get: function() {return this.$store.login},
set: function(val) {this.$store.commit("updateLogin", {login: val})}
},
media: {
get: function() {return this.$store.media},
set: function(val) {this.$store.commit("updateMedia", {media: val})}
}
},
methods: {
doChange() {
this.dataFiles = []
let fileList = document.getElementById(this.docid).files
this.files = Array.from(fileList)
if(this.files.length===0) return
this.files.map(v=>{
let file = this.$upload(v, this.vtype, this.$store.login.id)
this.dataFiles.push(file)
})
this.showmodal = {component: 'media/UploadProgress', title: 'Upload files', width: '700px', height: '200px', vbind: {files: this.dataFiles}}
this.clearFileList()
},
clearFileList() {
const fileInput = document.getElementById(this.docid)
const dt = new DataTransfer()
fileInput.files = dt.files
},
getFiles(files) {
this.data = files.concat(this.data)
setTimeout(()=>this.showmodal=undefined, 3000)
},
beginSearch(e) {
if(this.timer) clearTimeout(this.timer)
this.timer = setTimeout(() => this.startSearch(e.target.value), 150)
},
async startSearch(value) {
let filter = {user: this.login.id}
if(!this.$empty(value)) filter.name__icontains = value.toLowerCase()
this.data = await this.$getdata('file', filter)
},
displayInput() {
this.showUrl = true
this.url = undefined
setTimeout(()=>document.getElementById('url').focus(), 100)
},
checkUrl() {
if(this.$empty(this.url)) return this.$snackbar(`Đường dẫn không hợp lệ`, undefined, 'Error')
let self = this
this.loading = true
this.$axios.get(this.url, {responseType:"blob" })
.then(function(response) {
var reader = new window.FileReader()
reader.onload = (e) => {
self.image = e.target.result
setTimeout(()=> self.doUpload(e.target.result), 100)
}
reader.readAsDataURL(response.data)
self.loading = false
})
.catch(e => {
self.$buefy.toast.open({duration: 3000, message: `Đường dẫn không hợp lệ`, type: 'is-danger'})
self.loading = false
})
},
doUpload() {
const image = document.getElementById('image')
const canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
const ctx = canvas.getContext('2d')
ctx.drawImage(image, 0, 0)
if(canvas) canvas.toBlob(blod=>this.saveAs(blod))
},
async saveAs(blod) {
var form = new FormData();
const fileName = this.$dayjs(new Date()).format("YYYYMMDDhhmmss") + '-' + this.$id() + '.png'
form.append('filename', fileName)
form.append('name', `${this.$id()}.png`)
form.append('file', blod)
form.append('type', 'image')
form.append('size', 100)
form.append('user', this.$store.login.id)
let result = await this.$insertapi('upload', form)
if(result==='error') return
this.updateImage(result.rows[0])
this.showUrl = false
},
async uploadImage(file) {
this.loading = true
let result = await this.$insertapi('upload', file.form)
this.data.splice(0, 0, result.rows[0])
this.loading = false
},
getData(i) {
if(this.type==='video') {
var list = this.data.slice(i*5, i*5+5)
for (let index = 0; index < 5; index++) {
if(list.length<index+1) list.push({file: undefined})
}
} else {
list = this.data.slice(i*6, i*6+6)
for (let index = 0; index < 6; index++) {
if(list.length<index+1) list.push({file: undefined})
}
}
return list
},
selectMedia(v) {
let copy = this.media? this.$copy(this.media) : {}
copy.type = 'image'
copy.open = false
copy.select = v
this.media = copy
let row = this.$copy(v)
if(this.source) {
let params = {name: v.file, type: 'file'}
this.$axios.get(`${this.path}download/`, {params: params, responseType:"blob" })
.then(function (response) {
var reader = new window.FileReader()
reader.onload = (e) => {
fetch(e.target.result)
.then(res => res.blob())
.then(blob => {
const file = new File([blob], v.name,{ type: "image/png" })
row.source = {file: file}
})
}
reader.readAsDataURL(response.data)
})
}
this.$emit('modalevent', {name: 'selectimage', data: row})
},
editImage(v) {
this.loading = true
this.selected = v
let self = this
let params = {name: v.file, type: 'file'}
this.$axios.get(`${this.path}download/`, {params: params, responseType:"blob" })
.then(function (response) {
var reader = new window.FileReader()
reader.onload = (e) => {
self.image = e.target.result
self.showmodal = {component: 'media/CropImage', width: '65%', title: 'Cắt hình ảnh', vbind: {selected: self.selected, image: self.image}}
}
reader.readAsDataURL(response.data)
self.loading = false
})
},
copyMedia(v) {
this.$copyToClipboard(`${this.path}download/?name=${v.file}`)
},
deleteMedia(v, name) {
let self = this
var remove = async function() {
let result = await self.$deleteapi(name, v.id)
let idx = self.data.findIndex(x=>x.id===v.id)
self.$delete(self.data, idx)
}
this.$buefy.dialog.confirm({
message: 'Bạn muốn xóa file: ' + v.file,
onConfirm: () => remove()})
},
updateImage(v) {
this.data.splice(0, 0, v)
this.showmodal = undefined
}
}
}
</script>

View File

@@ -0,0 +1,54 @@
<template>
<div>
<div class="image-container" v-if="picture">
<img :src="picture" style="max-height: 250px !important;">
<div class="image-label">
<span class="is-clickable" @click="remove()">
<SvgIcon v-bind="{name: 'bin.svg', type: 'danger', size: 30}"></SvgIcon>
</span>
</div>
</div>
<div style="width: 130px; border-style: dashed; border-width: 1px;" v-else>
<a @click="openImage()"><SvgIcon v-bind="{name: 'image.svg', type: 'grey', size: 120}"></SvgIcon></a>
</div>
<div class="mt-2">
<a @click="openCamera()">
<SvgIcon v-bind="{name: 'camera.svg', type: 'dark', size: 28}"></SvgIcon>
</a>
</div>
<Modal @close="showmodal=undefined" v-bind="showmodal" v-if="showmodal" @selectimage="selectImage"></Modal>
</div>
</template>
<script>
export default {
props: ['file', 'image', 'show'],
data() {
return {
showmodal: undefined,
display: this.show || [],
picture: this.image || undefined,
vfile: undefined
}
},
methods: {
openImage() {
this.showmodal = {component:'media/Imagebox', title: 'Thư viện hình ảnh', width: '90%', vbind: {source: true}}
},
selectImage(files) {
this.showmodal = undefined
let v = files
this.picture = `${this.$path()}download/?name=${v.file__file || v.file}&type=file`
v.image = this.$copy(this.picture)
this.vfile = v
this.$emit('picture', v)
},
remove() {
this.vfile = undefined
this.picture = undefined
},
openCamera() {
this.showmodal = {component:'media/Camera', title: 'Chụp ảnh', width: '650px'}
}
}
}
</script>

View File

@@ -0,0 +1,72 @@
<template>
<div class="has-text-left">
<p class="py-1 border-bottom" v-for="v in vfiles">
<span class="icon-text">
<span class="mr-5">{{ v.name }}</span>
<SvgIcon
v-bind="{ name: 'check2.svg', type: 'primary', size: 22 }"
v-if="v.status === 'success'"
></SvgIcon>
<SvgIcon
v-bind="{ name: 'error.svg', type: 'danger', size: 22 }"
v-else-if="v.status === 'error'"
></SvgIcon>
</span>
<span class="icon-text has-text-danger ml-6" v-if="v.error">
<SvgIcon
v-bind="{ name: 'error.svg', type: 'danger', size: 22 }"
></SvgIcon>
<span class="ml-1">{{ v.text }}</span>
</span>
<button
class="button is-small is-white is-loading px-0 ml-4"
v-if="v.status === 'uploading'"
>
Loading
</button>
</p>
</div>
</template>
<script>
export default {
props: ["files"],
data() {
return {
vfiles: this.$copy(this.files),
data: [],
};
},
created() {
let found = this.$find(this.vfiles, { error: true });
if (!found) this.upload();
},
methods: {
async doUpload(v, i) {
let file = this.files[i];
let rs = await this.$insertapi("upload", file.form, undefined, false);
v.status = rs === "error" ? "error" : "success";
this.vfiles[i] = v;
let obj = rs.rows[0];
obj.source = file;
this.data.push(obj);
this.checkDone();
},
async upload() {
for (let i = 0; i < this.vfiles.length; i++) {
let v = this.vfiles[i];
v.status = "uploading";
await this.doUpload(v, i);
}
},
checkDone() {
let found = this.vfiles.find(
(v) => !v.status || v.status === "uploading"
);
if (!found) {
this.$emit("files", this.data);
this.$emit("modalevent", { name: "files", data: this.data });
}
},
},
};
</script>