feat(activities): attach compressed photos to activity notes
Photos on an activity are shrunk with the form-builder compressor and stored only when they fit under 200KB. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router';
|
||||
import { api } from '@/lib/api';
|
||||
import {
|
||||
@@ -35,6 +35,7 @@ const loading = ref(true);
|
||||
const saving = ref(false);
|
||||
const error = ref('');
|
||||
const activity = ref<ActivityDto | null>(null);
|
||||
const imageUrls = ref<Array<{ id: string; name: string; url: string }>>([]);
|
||||
const moreOpen = ref(false);
|
||||
const commentOpen = ref(false);
|
||||
const postponeOpen = ref(false);
|
||||
@@ -130,6 +131,7 @@ async function load() {
|
||||
try {
|
||||
const { data } = await api.get<ActivityDto>(`/activities/${id.value}`);
|
||||
activity.value = data;
|
||||
await loadActivityImages(data);
|
||||
assignUserIds.value = data.assignees?.length
|
||||
? data.assignees.map((u) => u.id)
|
||||
: data.assignee?.id
|
||||
@@ -143,6 +145,29 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function clearImageUrls() {
|
||||
for (const image of imageUrls.value) URL.revokeObjectURL(image.url);
|
||||
imageUrls.value = [];
|
||||
}
|
||||
|
||||
async function loadActivityImages(row: ActivityDto) {
|
||||
clearImageUrls();
|
||||
const next: Array<{ id: string; name: string; url: string }> = [];
|
||||
for (const image of row.images ?? []) {
|
||||
try {
|
||||
const { data } = await api.get<Blob>(`/activities/${row.id}/images/${image.id}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
next.push({ id: image.id, name: image.fileName, url: URL.createObjectURL(data) });
|
||||
} catch {
|
||||
/* عکس در دسترس نیست */
|
||||
}
|
||||
}
|
||||
imageUrls.value = next;
|
||||
}
|
||||
|
||||
onUnmounted(clearImageUrls);
|
||||
|
||||
async function loadUsers() {
|
||||
if (!canReassign.value) return;
|
||||
try {
|
||||
@@ -446,9 +471,15 @@ onMounted(() => {
|
||||
</strong>
|
||||
<UiIcon name="chevron" :size="16" :class="{ open: noteOpen }" />
|
||||
</button>
|
||||
<p v-show="noteOpen" :class="{ empty: !activity.description?.trim() }">
|
||||
{{ activity.description?.trim() || 'یادداشتی ثبت نشده.' }}
|
||||
</p>
|
||||
<div v-show="noteOpen">
|
||||
<p v-if="activity.description?.trim()">{{ activity.description.trim() }}</p>
|
||||
<p v-else-if="!imageUrls.length" class="empty">یادداشتی ثبت نشده.</p>
|
||||
<div v-if="imageUrls.length" class="note-photos">
|
||||
<a v-for="image in imageUrls" :key="image.id" :href="image.url" target="_blank" rel="noopener">
|
||||
<img :src="image.url" :alt="image.name" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
@@ -800,6 +831,26 @@ dd {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.note-photos {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.note-photos a {
|
||||
width: 5.5rem;
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
.note-photos img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 0.6rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.dock {
|
||||
position: fixed;
|
||||
inset-inline: 0.85rem;
|
||||
|
||||
Reference in New Issue
Block a user