feat: agencies => image upload (#67)

This commit is contained in:
puriphatt 2024-11-08 17:21:25 +07:00 committed by GitHub
parent 86a3247732
commit 4e622153ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 346 additions and 50 deletions

View file

@ -1,39 +1,70 @@
<script setup lang="ts"> <script setup lang="ts">
import DialogForm from 'src/components/DialogForm.vue'; import { reactive, ref, watch } from 'vue';
import { InstitutionPayload } from 'src/stores/institution/types';
import { Icon } from '@iconify/vue/dist/iconify.js';
import { useInstitution } from 'src/stores/institution';
import { baseUrl } from 'src/stores/utils';
import DrawerInfo from 'src/components/DrawerInfo.vue'; import DrawerInfo from 'src/components/DrawerInfo.vue';
import DialogForm from 'src/components/DialogForm.vue';
import ProfileBanner from 'src/components/ProfileBanner.vue'; import ProfileBanner from 'src/components/ProfileBanner.vue';
import SideMenu from 'src/components/SideMenu.vue'; import SideMenu from 'src/components/SideMenu.vue';
import FormBasicInfoAgencies from 'src/components/07_agencies-management/FormBasicInfoAgencies.vue'; import FormBasicInfoAgencies from 'src/components/07_agencies-management/FormBasicInfoAgencies.vue';
import AddressForm from 'src/components/form/AddressForm.vue';
import { import {
UndoButton, UndoButton,
SaveButton, SaveButton,
EditButton, EditButton,
DeleteButton, DeleteButton,
} from 'src/components/button'; } from 'src/components/button';
import { InstitutionPayload } from 'src/stores/institution/types'; import AddressForm from 'src/components/form/AddressForm.vue';
import ImageUploadDialog from 'src/components/ImageUploadDialog.vue';
const institutionStore = useInstitution();
const model = defineModel<boolean>({ required: true, default: false }); const model = defineModel<boolean>({ required: true, default: false });
const drawerModel = defineModel<boolean>('drawerModel', { const drawerModel = defineModel<boolean>('drawerModel', {
required: true, required: true,
default: false, default: false,
}); });
const onCreateImageList = defineModel<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>('onCreateImageList', { default: { selectedImage: '', list: [] } });
withDefaults( const imageState = reactive({
imageDialog: false,
isImageEdit: false,
refreshImageState: false,
imageUrl: '',
});
const imageFile = ref<File | null>(null);
const imageList = ref<{ selectedImage: string; list: string[] }>();
const props = withDefaults(
defineProps<{ defineProps<{
readonly?: boolean; readonly?: boolean;
isEdit?: boolean; isEdit?: boolean;
dataId?: string;
}>(), }>(),
{ readonly: false, isEdit: false }, { readonly: false, isEdit: false, dataId: '' },
); );
defineEmits<{ defineExpose({ clearImageState });
const emit = defineEmits<{
(e: 'submit'): void; (e: 'submit'): void;
(e: 'close'): void; (e: 'close'): void;
(e: 'changeStatus'): void; (e: 'changeStatus'): void;
(e: 'drawerUndo'): void; (e: 'drawerUndo'): void;
(e: 'drawerEdit'): void; (e: 'drawerEdit'): void;
(e: 'drawerDelete'): void; (e: 'drawerDelete'): void;
(e: 'addImage'): void;
(e: 'removeImage'): void;
(e: 'submitImage', name: string): void;
}>(); }>();
const data = defineModel<InstitutionPayload>('data', { const data = defineModel<InstitutionPayload>('data', {
@ -54,8 +85,96 @@ const data = defineModel<InstitutionPayload>('data', {
subDistrictId: '', subDistrictId: '',
districtId: '', districtId: '',
provinceId: '', provinceId: '',
selectedImage: '',
}, },
}); });
function viewImage() {
imageState.imageDialog = true;
imageState.isImageEdit = false;
}
function editImage() {
imageState.imageDialog = imageState.isImageEdit = true;
}
async function fetchImageList(id: string, selectedName: string) {
const res = await institutionStore.fetchImageListById(id);
imageList.value = {
selectedImage: selectedName,
list: res.map((n: string) => `institution/${id}/image/${n}`),
};
return res;
}
async function addImage(file: File | null) {
if (!file) return;
if (!props.dataId) return;
await institutionStore.addImageList(
file,
props.dataId,
Date.now().toString(),
);
await fetchImageList(props.dataId, data.value.selectedImage || '');
}
async function removeImage(name: string) {
if (!name) return;
if (!props.dataId) return;
const targetName = name.split('/').pop() || '';
await institutionStore.deleteImageByName(props.dataId, targetName);
await fetchImageList(props.dataId, data.value.selectedImage || '');
}
async function submitImage(name: string) {
if (model.value) {
imageState.imageUrl = name;
imageState.imageDialog = false;
} else {
imageState.refreshImageState = true;
emit('submitImage', name);
}
}
function clearImageState() {
imageState.imageDialog = false;
imageFile.value = null;
onCreateImageList.value = { selectedImage: '', list: [] };
imageState.refreshImageState = false;
}
watch(
() => imageFile.value,
() => {
if (imageFile.value !== null) imageState.isImageEdit = true;
},
);
watch(
() => data.value.selectedImage,
() => {
imageState.imageUrl = `${baseUrl}/institution/${props.dataId}/image/${data.value.selectedImage}`;
imageList.value
? (imageList.value.selectedImage = data.value.selectedImage || '')
: '';
console.log(imageState.imageUrl);
imageState.refreshImageState = false;
},
);
watch(
() => drawerModel.value,
async () => {
if (drawerModel.value) {
await fetchImageList(props.dataId, data.value.selectedImage || '');
imageState.imageUrl = `${baseUrl}/institution/${props.dataId}/image/${data.value.selectedImage}`;
} else {
imageList.value = { selectedImage: '', list: [] };
}
},
);
</script> </script>
<template> <template>
<DialogForm <DialogForm
@ -63,7 +182,13 @@ const data = defineModel<InstitutionPayload>('data', {
:title="$t('general.add', { text: $t('agencies.title') })" :title="$t('general.add', { text: $t('agencies.title') })"
v-model:modal="model" v-model:modal="model"
:submit="() => $emit('submit')" :submit="() => $emit('submit')"
:close="() => $emit('close')" :close="
() => {
imageState.imageUrl = '';
clearImageState();
$emit('close');
}
"
> >
<div <div
:class="{ :class="{
@ -72,24 +197,18 @@ const data = defineModel<InstitutionPayload>('data', {
}" }"
> >
<ProfileBanner <ProfileBanner
no-image-action
readonly
active active
hide-fade hide-fade
hide-active hide-active
:icon="'ph-building-office'" :icon="'ph-building-office'"
:img="imageState.imageUrl || null"
:title="data.name" :title="data.name"
:caption="data.code" :caption="data.code"
:color="`hsla(var(--green-8-hsl)/1)`" :color="`hsla(var(--green-8-hsl)/1)`"
:bg-color="`hsla(var(--green-8-hsl)/0.1)`" :bg-color="`hsla(var(--green-8-hsl)/0.1)`"
@view="viewImage"
@edit="editImage"
/> />
<!-- v-model:toggle-status="currentStatusGroupType"
@update:toggle-status="
() => {
currentStatusGroupType =
currentStatusGroupType === 'CREATED' ? 'INACTIVE' : 'CREATED';
}
" -->
</div> </div>
<div <div
@ -177,7 +296,12 @@ const data = defineModel<InstitutionPayload>('data', {
:title="data.name" :title="data.name"
v-model:drawerOpen="drawerModel" v-model:drawerOpen="drawerModel"
:submit="() => $emit('submit')" :submit="() => $emit('submit')"
:close="() => $emit('close')" :close="
() => {
clearImageState();
$emit('close');
}
"
> >
<div class="col column full-height"> <div class="col column full-height">
<div <div
@ -187,8 +311,6 @@ const data = defineModel<InstitutionPayload>('data', {
}" }"
> >
<ProfileBanner <ProfileBanner
no-image-action
readonly
active active
hide-fade hide-fade
hide-active hide-active
@ -197,6 +319,13 @@ const data = defineModel<InstitutionPayload>('data', {
:caption="data.code" :caption="data.code"
:color="`hsla(var(${'--green-8'}-hsl)/1)`" :color="`hsla(var(${'--green-8'}-hsl)/1)`"
:bg-color="`hsla(var(${'--green-8'}-hsl)/0.1)`" :bg-color="`hsla(var(${'--green-8'}-hsl)/0.1)`"
:img="
`${baseUrl}/institution/${dataId}/image/${data.selectedImage}`.concat(
imageState.refreshImageState ? `?ts=${Date.now()}` : '',
) || null
"
@view="viewImage"
@edit="editImage"
/> />
</div> </div>
@ -330,5 +459,39 @@ const data = defineModel<InstitutionPayload>('data', {
</div> </div>
</div> </div>
</DrawerInfo> </DrawerInfo>
<ImageUploadDialog
v-model:dialog-state="imageState.imageDialog"
v-model:file="imageFile"
v-model:on-create-data-list="onCreateImageList"
v-model:image-url="imageState.imageUrl"
v-model:data-list="imageList"
:on-create="model"
:hiddenFooter="!imageState.isImageEdit"
@add-image="addImage"
@remove-image="removeImage"
@submit="submitImage"
>
<template #title>
<span v-if="!model" class="justify-center flex text-bold">
{{ $t('general.image') }}
{{ $i18n.locale === 'eng' ? data.nameEN : data.name }}
</span>
</template>
<template #error>
<div class="full-height full-width" style="background: white">
<div
class="full-height full-width flex justify-center items-center"
style="
background: hsla(var(--green-8-hsl) / 0.1);
color: hsla(var(--green-8-hsl) / 1);
"
>
<Icon width="3rem" icon="ph-building-office" />
</div>
</div>
</template>
</ImageUploadDialog>
</template> </template>
<style scoped></style> <style scoped></style>

View file

@ -6,6 +6,7 @@ import { storeToRefs } from 'pinia';
import { Icon } from '@iconify/vue/dist/iconify.js'; import { Icon } from '@iconify/vue/dist/iconify.js';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { baseUrl } from 'src/stores/utils';
import { useNavigator } from 'src/stores/navigator'; import { useNavigator } from 'src/stores/navigator';
import { useInstitution } from 'src/stores/institution'; import { useInstitution } from 'src/stores/institution';
import { Institution, InstitutionPayload } from 'src/stores/institution/types'; import { Institution, InstitutionPayload } from 'src/stores/institution/types';
@ -95,9 +96,16 @@ const blankFormData: InstitutionPayload = {
subDistrictId: '', subDistrictId: '',
districtId: '', districtId: '',
provinceId: '', provinceId: '',
selectedImage: '',
}; };
const refAgenciesDialog = ref();
const formData = ref<InstitutionPayload>(structuredClone(blankFormData)); const formData = ref<InstitutionPayload>(structuredClone(blankFormData));
const currAgenciesData = ref<Institution>(); const currAgenciesData = ref<Institution>();
const onCreateImageList = ref<{
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
}>({ selectedImage: '', list: [] });
function triggerDialog(type: 'add' | 'edit' | 'view') { function triggerDialog(type: 'add' | 'edit' | 'view') {
if (type === 'add') { if (type === 'add') {
@ -146,10 +154,11 @@ function assignFormData(data: Institution) {
subDistrictId: data.subDistrictId, subDistrictId: data.subDistrictId,
districtId: data.districtId, districtId: data.districtId,
provinceId: data.provinceId, provinceId: data.provinceId,
selectedImage: data.selectedImage,
}; };
} }
async function submit() { async function submit(opt?: { selectedImage: string }) {
const payload = { const payload = {
group: undefined, group: undefined,
code: formData.value.code, code: formData.value.code,
@ -167,26 +176,41 @@ async function submit() {
districtId: formData.value.districtId, districtId: formData.value.districtId,
provinceId: formData.value.provinceId, provinceId: formData.value.provinceId,
}; };
if (
if (pageState.isDrawerEdit && currAgenciesData.value?.id) { (pageState.isDrawerEdit && currAgenciesData.value?.id) ||
(opt?.selectedImage && currAgenciesData.value?.id)
) {
const ret = await institutionStore.editInstitution( const ret = await institutionStore.editInstitution(
Object.assign(payload, { id: currAgenciesData.value.id }), Object.assign(payload, {
id: currAgenciesData.value.id,
selectedImage: opt?.selectedImage || undefined,
}),
); );
if (ret) { if (ret) {
pageState.isDrawerEdit = false; pageState.isDrawerEdit = false;
currAgenciesData.value = ret; currAgenciesData.value = ret;
formData.value.selectedImage = ret.selectedImage;
await fetchData(); await fetchData();
if (refAgenciesDialog.value && opt?.selectedImage) {
refAgenciesDialog.value.clearImageState();
}
return; return;
} }
} else {
await institutionStore.createInstitution(
{
...payload,
code: formData.value.group || '',
},
onCreateImageList.value,
);
await fetchData();
pageState.addModal = false;
return;
} }
await institutionStore.createInstitution({
...payload,
code: formData.value.group || '',
});
await fetchData();
pageState.addModal = false;
} }
async function triggerChangeStatus(data?: Institution) {} async function triggerChangeStatus(data?: Institution) {}
@ -409,7 +433,7 @@ watch(
<!-- SEC: body content --> <!-- SEC: body content -->
<article <article
v-if="false" v-if="data.length === 0"
class="col surface-2 flex items-center justify-center" class="col surface-2 flex items-center justify-center"
> >
<NoData <NoData
@ -476,14 +500,24 @@ watch(
</q-td> </q-td>
<q-td v-if="fieldSelected.includes('name')"> <q-td v-if="fieldSelected.includes('name')">
<section class="row items-center no-wrap"> <section class="row items-center no-wrap">
<q-avatar <q-avatar size="md">
size="md" <q-img
style=" class="text-center"
background: hsla(var(--green-8-hsl) / 0.1); :ratio="1"
color: hsla(var(--green-8-hsl) / 1); :src="`${baseUrl}/institution/${props.row.id}/image/${props.row.selectedImage}?ts=${Date.now()}`"
" >
> <template #error>
<Icon icon="ph-building-office" /> <div
class="no-padding full-width full-height flex items-center justify-center"
style="
background: hsla(var(--green-8-hsl) / 0.1);
color: hsla(var(--green-8-hsl) / 1);
"
>
<Icon icon="ph-building-office" />
</div>
</template>
</q-img>
</q-avatar> </q-avatar>
<span class="col q-pl-md"> <span class="col q-pl-md">
<div> <div>
@ -558,14 +592,24 @@ watch(
<section class="column col-12 col-md-6"> <section class="column col-12 col-md-6">
<div class="bordered col surface-1 rounded q-pa-md"> <div class="bordered col surface-1 rounded q-pa-md">
<header class="row items-center"> <header class="row items-center">
<q-avatar <q-avatar size="xl">
size="xl" <q-img
style=" class="text-center"
background: hsla(var(--green-8-hsl) / 0.1); :ratio="1"
color: hsla(var(--green-8-hsl) / 1); :src="`${baseUrl}/institution/${props.row.id}/image/${props.row.selectedImage}?ts=${Date.now()}`"
" >
> <template #error>
<Icon icon="ph-building-office" /> <div
class="no-padding full-width full-height flex items-center justify-center"
style="
background: hsla(var(--green-8-hsl) / 0.1);
color: hsla(var(--green-8-hsl) / 1);
"
>
<Icon icon="ph-building-office" />
</div>
</template>
</q-img>
</q-avatar> </q-avatar>
<span class="text-weight-bold column q-pl-md"> <span class="text-weight-bold column q-pl-md">
{{ {{
@ -697,6 +741,8 @@ watch(
</div> </div>
<AgenciesDialog <AgenciesDialog
ref="refAgenciesDialog"
:data-id="currAgenciesData && currAgenciesData.id"
@change-status="triggerChangeStatus" @change-status="triggerChangeStatus"
@drawer-delete=" @drawer-delete="
() => { () => {
@ -707,11 +753,17 @@ watch(
@drawer-undo="undo" @drawer-undo="undo"
@close="resetForm" @close="resetForm"
@submit="submit" @submit="submit"
@submit-image="
async (v) => {
if (v) await submit({ selectedImage: v });
}
"
:readonly="!pageState.isDrawerEdit" :readonly="!pageState.isDrawerEdit"
:isEdit="pageState.isDrawerEdit" :isEdit="pageState.isDrawerEdit"
v-model="pageState.addModal" v-model="pageState.addModal"
v-model:drawer-model="pageState.viewDrawer" v-model:drawer-model="pageState.viewDrawer"
v-model:data="formData" v-model:data="formData"
v-model:on-create-image-list="onCreateImageList"
/> />
</template> </template>
<style scoped> <style scoped>

View file

@ -3,8 +3,11 @@ import { ref } from 'vue';
import { Institution, InstitutionPayload } from './types'; import { Institution, InstitutionPayload } from './types';
import { api } from 'src/boot/axios'; import { api } from 'src/boot/axios';
import { PaginationResult } from 'src/types'; import { PaginationResult } from 'src/types';
import useFlowStore from '../flow';
export const useInstitution = defineStore('institution-store', () => { export const useInstitution = defineStore('institution-store', () => {
const flowStore = useFlowStore();
const data = ref<Institution[]>([]); const data = ref<Institution[]>([]);
const page = ref<number>(1); const page = ref<number>(1);
const pageMax = ref<number>(1); const pageMax = ref<number>(1);
@ -33,8 +36,26 @@ export const useInstitution = defineStore('institution-store', () => {
return null; return null;
} }
async function createInstitution(data: InstitutionPayload) { async function createInstitution(
const res = await api.post('/institution', data); data: InstitutionPayload,
imgList: {
selectedImage: string;
list: { url: string; imgFile: File | null; name: string }[];
},
) {
const res = await api.post('/institution', {
...data,
selectedImage: imgList.selectedImage || '',
});
if (imgList.list.length > 0 && res.data.id) {
for (let index = 0; index < imgList.list.length; index++) {
const imgFile = imgList.list[index].imgFile;
if (imgFile)
await addImageList(imgFile, res.data.id, imgList.list[index].name);
}
}
if (res.status < 400) { if (res.status < 400) {
return res.data; return res.data;
} }
@ -60,6 +81,60 @@ export const useInstitution = defineStore('institution-store', () => {
return null; return null;
} }
async function fetchImageListById(
id: string,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const res = await api.get(`/institution/${id}/image`, {
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
});
if (!res) return false;
if (res.status === 200) return res.data;
if (res.status === 204) return null;
return false;
}
async function addImageList(file: File, id: string, name: string) {
await api
.put(`/institution/${id}/image/${name}`, file, {
headers: { 'Content-Type': file.type },
onUploadProgress: (e) => console.log(e),
})
.catch((e) => console.error(e));
return name;
}
async function deleteImageByName(
id: string,
name: string,
flow?: {
sessionId?: string;
refTransactionId?: string;
transactionId?: string;
},
) {
const res = await api.delete(`/institution/${id}/image/${name}`, {
headers: {
'X-Session-Id': flow?.sessionId,
'X-Rtid': flow?.refTransactionId || flowStore.rtid,
'X-Tid': flow?.transactionId,
},
});
if (!res) return false;
}
return { return {
data, data,
page, page,
@ -71,5 +146,9 @@ export const useInstitution = defineStore('institution-store', () => {
createInstitution, createInstitution,
editInstitution, editInstitution,
deleteInstitution, deleteInstitution,
fetchImageListById,
addImageList,
deleteImageByName,
}; };
}); });

View file

@ -6,6 +6,7 @@ export type Institution = {
group: string; group: string;
name: string; name: string;
nameEN: string; nameEN: string;
selectedImage?: string | null;
addressEN: string; addressEN: string;
address: string; address: string;
@ -29,6 +30,7 @@ export type InstitutionPayload = {
name: string; name: string;
nameEN: string; nameEN: string;
group?: string; group?: string;
selectedImage?: string | null;
addressEN: string; addressEN: string;
address: string; address: string;