mirror of
https://github.com/MarcZierle/photo-log-frontend.git
synced 2025-04-07 21:14:37 +00:00
add photo management page
This commit is contained in:
parent
29c6777a03
commit
08b8e0c35c
@ -7,7 +7,9 @@
|
|||||||
<n-config-provider :theme="null">
|
<n-config-provider :theme="null">
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<NavBar v-if="doesntNeedNav" />
|
<NavBar v-if="doesntNeedNav" />
|
||||||
<router-view />
|
<div :class="{'body_wrapper' : doesntNeedNav}">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
</n-message-provider>
|
</n-message-provider>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -53,4 +55,9 @@ body,html {
|
|||||||
font-family: v-sans;
|
font-family: v-sans;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.body_wrapper {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -3,7 +3,7 @@ import axios from 'axios'
|
|||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: 'https://server.riezel.com/api/v1',
|
baseURL: 'https://server.riezel.com/api/v1',
|
||||||
withCredentials: false,
|
withCredentials: false,
|
||||||
timeout: 10000,
|
timeout: 120000,
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@ -80,4 +80,8 @@ export function addNewPhoto(original_image, cropped_image=null, group_id=null, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
return apiClient.post('/addphoto/', data, config)
|
return apiClient.post('/addphoto/', data, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePhoto(photo) {
|
||||||
|
return apiClient.put('/updatephoto/'+photo.id+'/', photo)
|
||||||
}
|
}
|
266
src/components/PhotoItem.vue
Normal file
266
src/components/PhotoItem.vue
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
:style="card_styles"
|
||||||
|
@mouseover="setHoverIfLoaded"
|
||||||
|
@mouseleave="hover=false"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="cover"
|
||||||
|
:style="cover_styles"
|
||||||
|
ref="cover"
|
||||||
|
@click="toggleSelection"
|
||||||
|
>
|
||||||
|
<n-skeleton v-if="!isImgLoaded" :height="height" :width="width" />
|
||||||
|
<div v-if="isImgLoaded" class="selection" :style="selection_styles">
|
||||||
|
<n-icon size="4em" color="rgba(255,255,255,0.75)"><Check /></n-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="options"
|
||||||
|
:style="options_styles"
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<div class="options-item" v-if="can_change_group">
|
||||||
|
<n-button text @click="$emit('update:group')">
|
||||||
|
<template #icon><n-icon><SortFilled /></n-icon></template>
|
||||||
|
Change Group
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options-item" v-if="can_crop">
|
||||||
|
<n-button text @click="$emit('update:crop')">
|
||||||
|
<template #icon><n-icon><CropRotateFilled /></n-icon></template>
|
||||||
|
Crop Photo
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options-item" v-if="can_change_ocr">
|
||||||
|
<n-button text @click="$emit('update:ocr')">
|
||||||
|
<template #icon><n-icon><TextAnnotationToggle /></n-icon></template>
|
||||||
|
Change OCR
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="options-item" v-if="can_delete">
|
||||||
|
<n-button text type="error" @click="$emit('update:delete')">
|
||||||
|
<template #icon><n-icon><TrashBinSharp /></n-icon></template>
|
||||||
|
Delete Photo
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TextAnnotationToggle from '@vicons/carbon/TextAnnotationToggle'
|
||||||
|
import TrashBinSharp from '@vicons/ionicons5/TrashBinSharp'
|
||||||
|
import SortFilled from '@vicons/material/SortFilled'
|
||||||
|
import CropRotateFilled from '@vicons/material/CropRotateFilled'
|
||||||
|
import Check from '@vicons/fa/Check'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
TextAnnotationToggle,
|
||||||
|
TrashBinSharp,
|
||||||
|
SortFilled,
|
||||||
|
CropRotateFilled,
|
||||||
|
Check,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
can_change_group: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
can_select: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
can_change_ocr: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
can_crop: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
can_delete: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 150,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: [
|
||||||
|
'update:select',
|
||||||
|
'update:delet',
|
||||||
|
'update:crop',
|
||||||
|
'update:ocr',
|
||||||
|
'update:group',
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hover: false,
|
||||||
|
selected: false,
|
||||||
|
|
||||||
|
isImgLoaded: false,
|
||||||
|
imgSrcObj: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeMount() {
|
||||||
|
this.imgSrcObj = new Image()
|
||||||
|
this.imgSrcObj.src = this.src
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
imgSrcObj: {
|
||||||
|
handler: function () {
|
||||||
|
if (this.imgSrcObj.complete) {
|
||||||
|
URL.revokeObjectURL(this.imgSrcObj.src)
|
||||||
|
this.isImgLoaded = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
height() { return this.width * 1.41421356 },
|
||||||
|
options_height() {
|
||||||
|
return [
|
||||||
|
this.can_change_group,
|
||||||
|
this.can_crop,
|
||||||
|
this.can_change_ocr,
|
||||||
|
this.can_delete
|
||||||
|
].filter(Boolean).length * 35
|
||||||
|
},
|
||||||
|
card_styles() {
|
||||||
|
if (this.hover) {
|
||||||
|
return `
|
||||||
|
z-index: 90;
|
||||||
|
box-shadow: 0px 0px 13px 5px rgba(0,0,0,0.05);
|
||||||
|
${/*transform: translateY(-10px);*/{}}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
cover_styles() {
|
||||||
|
let styles = `
|
||||||
|
background-image: url(${this.src});
|
||||||
|
width: ${this.width}px;`
|
||||||
|
|
||||||
|
|
||||||
|
if (this.hover) {
|
||||||
|
styles += `height: ${this.height - this.options_height + 5}px;`
|
||||||
|
} else {
|
||||||
|
styles += `height: ${this.height}px;`
|
||||||
|
}
|
||||||
|
|
||||||
|
return styles
|
||||||
|
},
|
||||||
|
selection_styles() {
|
||||||
|
if (this.selected) {
|
||||||
|
return 'opacity: 100%;'
|
||||||
|
}
|
||||||
|
return 'opacity: 0;'
|
||||||
|
},
|
||||||
|
options_styles() {
|
||||||
|
const base_height = 5
|
||||||
|
const expand_height = this.options_height
|
||||||
|
|
||||||
|
if (!this.hover) {
|
||||||
|
return `
|
||||||
|
height: ${base_height}px;
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
return `
|
||||||
|
height: ${expand_height}px;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setHoverIfLoaded() {
|
||||||
|
if (this.isImgLoaded) {
|
||||||
|
this.hover = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleSelection() {
|
||||||
|
if (this.can_select) {
|
||||||
|
this.selected = !this.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card{
|
||||||
|
transition: all 0.25s;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
transition: all 0.25s;
|
||||||
|
border: 1px solid rgb(238, 238, 238);
|
||||||
|
border-top: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options .content {
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options .options-item {
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.125em 1em 0.125em 1em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options .options-item:hover {
|
||||||
|
background-color: rgb(245, 245, 245);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 162, 255, 0.432);
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection .n-icon {
|
||||||
|
transform: translateY(50%);
|
||||||
|
}
|
||||||
|
</style>
|
@ -57,9 +57,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
//import PhotoItem from '@/components/PhotoItem'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PhotoSelectModal',
|
name: 'PhotoSelectModal',
|
||||||
emits: [ 'closed', 'selected' ],
|
emits: [ 'closed', 'selected' ],
|
||||||
|
components: {
|
||||||
|
//PhotoItem
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
showSelection: {
|
showSelection: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -5,6 +5,7 @@ import HomeView from '../views/Home.vue'
|
|||||||
import LogsList from '../views/LogsList.vue'
|
import LogsList from '../views/LogsList.vue'
|
||||||
import CreateLog from '../views/CreateLog.vue'
|
import CreateLog from '../views/CreateLog.vue'
|
||||||
import CameraCapture from '../views/CameraCapture.vue'
|
import CameraCapture from '../views/CameraCapture.vue'
|
||||||
|
import ManagePhotos from '../views/ManagePhotos.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -34,6 +35,11 @@ const routes = [
|
|||||||
name: 'CameraCapture',
|
name: 'CameraCapture',
|
||||||
component: CameraCapture
|
component: CameraCapture
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/photos',
|
||||||
|
name: 'ManagePhotos',
|
||||||
|
component: ManagePhotos
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
@ -6,14 +6,15 @@ import {
|
|||||||
getPhotoLog,
|
getPhotoLog,
|
||||||
updatePhotoLog,
|
updatePhotoLog,
|
||||||
getPhotoGroups,
|
getPhotoGroups,
|
||||||
getPhotosByGroup
|
getPhotosByGroup,
|
||||||
|
updatePhoto,
|
||||||
} from '@/api'
|
} from '@/api'
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
state: {
|
state: {
|
||||||
photoLogList: [],
|
photoLogList: [],
|
||||||
photoLogs: [],
|
photoLogs: [],
|
||||||
photoGroups: [],
|
photoGroups: null,
|
||||||
photos: []
|
photos: []
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
@ -41,6 +42,22 @@ export default createStore({
|
|||||||
} else {
|
} else {
|
||||||
state.photos = {...state.photos, [group_id]: photos}
|
state.photos = {...state.photos, [group_id]: photos}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
UPDATE_PHOTO(state, photo) {
|
||||||
|
for (const idx in state.photos) {
|
||||||
|
let group = state.photos[idx]
|
||||||
|
let photo_index = group.findIndex(p => p.id == photo.id)
|
||||||
|
if (photo_index > -1) {
|
||||||
|
let state_photo = state.photos[idx].splice(photo_index, 1)[0]
|
||||||
|
state_photo.group = photo.group
|
||||||
|
state_photo.bbox_coords = photo.bbox_coords
|
||||||
|
state_photo.ocr_text = photo.ocr_text
|
||||||
|
if (!state.photos[photo.group] || state.photos[photo.group].length === 0) {
|
||||||
|
state.photos[photo.group] = []
|
||||||
|
}
|
||||||
|
state.photos[photo.group].push(state_photo)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@ -115,6 +132,16 @@ export default createStore({
|
|||||||
const group_id = getters.photoGroups[index].id
|
const group_id = getters.photoGroups[index].id
|
||||||
dispatch('loadPhotosInGroup', group_id)
|
dispatch('loadPhotosInGroup', group_id)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
updatePhoto({commit}, photo) {
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
updatePhoto(photo).then(() => {
|
||||||
|
commit('UPDATE_PHOTO', photo)
|
||||||
|
resolve()
|
||||||
|
}).catch((error) => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
@ -34,7 +34,9 @@
|
|||||||
<n-icon><CameraAdd20Filled /></n-icon>
|
<n-icon><CameraAdd20Filled /></n-icon>
|
||||||
</n-button>
|
</n-button>
|
||||||
</router-link>
|
</router-link>
|
||||||
<n-button>Crop Photos</n-button>
|
<router-link :to="{name: 'ManagePhotos'}">
|
||||||
|
<n-button>Manage Photos</n-button>
|
||||||
|
</router-link>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
149
src/views/ManagePhotos.vue
Normal file
149
src/views/ManagePhotos.vue
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Manage Photos</h1>
|
||||||
|
<n-collapse
|
||||||
|
v-if="photoGroups && photoGroups.length > 0"
|
||||||
|
@item-header-click="loadPhotosInGroup"
|
||||||
|
>
|
||||||
|
<n-collapse-item
|
||||||
|
v-for="group in photoGroups" :key="group.id"
|
||||||
|
:title="group.name"
|
||||||
|
:name="group.id">
|
||||||
|
<template #header-extra>{{ group.date }}</template>
|
||||||
|
|
||||||
|
<p v-if="!photos[group.id]">loading...</p>
|
||||||
|
|
||||||
|
<n-space>
|
||||||
|
<PhotoItem
|
||||||
|
v-for="(photo, idx) in photos[group.id]" :key="idx"
|
||||||
|
:src="getPhotoSrcById(photo.id)"
|
||||||
|
:can_change_group="true"
|
||||||
|
:can_crop="true"
|
||||||
|
:can_change_ocr="true"
|
||||||
|
:can_delete="true"
|
||||||
|
@update:group="change_group_modal(photo.id)"
|
||||||
|
/>
|
||||||
|
</n-space>
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
<p v-else>No Photo Groups</p>
|
||||||
|
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showChangeGroupModal"
|
||||||
|
:mask-closable="true" >
|
||||||
|
<n-card
|
||||||
|
style="width: 400px;"
|
||||||
|
title="Change Photo Group"
|
||||||
|
:bordered="false"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
<n-select v-model:value="new_group" :options="groups_select_options" />
|
||||||
|
<template #footer>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="showChangeGroupModal=false">Cancel</n-button>
|
||||||
|
<n-button type="success" @click="changePhotoGroup">Change Group</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useMeta } from 'vue-meta'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
import PhotoItem from '@/components/PhotoItem'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
PhotoItem,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
useMeta({ title: 'Manage Photos' })
|
||||||
|
const message = useMessage()
|
||||||
|
return { message }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
current_photo: null,
|
||||||
|
|
||||||
|
showChangeGroupModal: false,
|
||||||
|
new_group: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
photoGroups() {
|
||||||
|
let groups = this.$store.state.photoGroups
|
||||||
|
if (groups !== null) {
|
||||||
|
groups.sort((a,b) => {
|
||||||
|
return a.date < b.date ? 1 : -1
|
||||||
|
})
|
||||||
|
let index = groups.indexOf(groups.filter(g=>g.id==1)[0])
|
||||||
|
groups.unshift(groups.splice(index, 1)[0])
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
photos() {
|
||||||
|
return this.$store.state.photos
|
||||||
|
},
|
||||||
|
groups_select_options() {
|
||||||
|
if (this.photoGroups.length > 0) {
|
||||||
|
let options = []
|
||||||
|
for (const idx in this.photoGroups) {
|
||||||
|
options.push({
|
||||||
|
value: this.photoGroups[idx].id,
|
||||||
|
label: this.photoGroups[idx].name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('loadPhotoGroups').catch((error) => {
|
||||||
|
this.message.error('Cannot load photo groups: ' + error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadPhotosInGroup(group_data) {
|
||||||
|
if (group_data.expanded)
|
||||||
|
this.$store.dispatch('loadPhotosInGroup', group_data.name)
|
||||||
|
},
|
||||||
|
getPhotoSrcById(photo_id){
|
||||||
|
for (const index in this.photos) {
|
||||||
|
let group = this.photos[index]
|
||||||
|
let photo = group.filter((photo) => photo.id == photo_id)
|
||||||
|
if (photo.length > 0) {
|
||||||
|
photo = photo[0]
|
||||||
|
return photo.cropped_image !== null ? photo.cropped_image : photo.original_image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
change_group_modal(photo_id) {
|
||||||
|
this.current_photo = photo_id
|
||||||
|
this.showChangeGroupModal = true
|
||||||
|
this.new_group = null
|
||||||
|
},
|
||||||
|
changePhotoGroup(){
|
||||||
|
if (this.new_group && this.current_photo) {
|
||||||
|
this.$store.dispatch('updatePhoto', {
|
||||||
|
id: this.current_photo,
|
||||||
|
group: this.new_group
|
||||||
|
}).then(()=>{
|
||||||
|
this.current_photo = null
|
||||||
|
this.new_group = null
|
||||||
|
this.showChangeGroupModal = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user