120 lines
3.7 KiB
Vue
120 lines
3.7 KiB
Vue
<script setup lang="ts">
|
||||
|
|
import { onMounted, ref } from 'vue';
|
|||
|
|
import { RouterLink } from 'vue-router';
|
|||
|
|
import axios from 'axios';
|
|||
|
|
import { api } from '@/lib/api';
|
|||
|
|
import { formatJalaliNumeric } from '@/lib/jalali';
|
|||
|
|
import type { NoteAttachmentDto, NoteDto, PaginatedResponse } from '@mento/shared';
|
|||
|
|
|
|||
|
|
type Row = NoteAttachmentDto & {
|
|||
|
|
noteId: string;
|
|||
|
|
noteTitle: string;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const loading = ref(false);
|
|||
|
|
const error = ref('');
|
|||
|
|
const items = ref<Row[]>([]);
|
|||
|
|
|
|||
|
|
function sizeLabel(bytes: number) {
|
|||
|
|
if (bytes < 1024) return `${bytes} B`;
|
|||
|
|
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
|
|||
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function load() {
|
|||
|
|
loading.value = true;
|
|||
|
|
error.value = '';
|
|||
|
|
try {
|
|||
|
|
const { data } = await api.get<PaginatedResponse<NoteDto>>('/notes', {
|
|||
|
|
params: { limit: 100 },
|
|||
|
|
});
|
|||
|
|
items.value = data.items.flatMap((note) =>
|
|||
|
|
(note.attachments ?? []).map((att) => ({
|
|||
|
|
...att,
|
|||
|
|
noteId: note.id,
|
|||
|
|
noteTitle: note.title?.trim() || note.body.slice(0, 40) || 'یادداشت',
|
|||
|
|
})),
|
|||
|
|
);
|
|||
|
|
} catch (e) {
|
|||
|
|
error.value = axios.isAxiosError(e)
|
|||
|
|
? 'بارگذاری پیوستها ناموفق بود.'
|
|||
|
|
: 'بارگذاری پیوستها ناموفق بود.';
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function download(row: Row) {
|
|||
|
|
const { data } = await api.get<Blob>(
|
|||
|
|
`/notes/${row.noteId}/attachments/${row.id}/download`,
|
|||
|
|
{ responseType: 'blob' },
|
|||
|
|
);
|
|||
|
|
const url = URL.createObjectURL(data);
|
|||
|
|
const a = document.createElement('a');
|
|||
|
|
a.href = url;
|
|||
|
|
a.download = row.fileName;
|
|||
|
|
a.click();
|
|||
|
|
URL.revokeObjectURL(url);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(load);
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<section class="page" dir="rtl">
|
|||
|
|
<header class="head">
|
|||
|
|
<div>
|
|||
|
|
<h1>پیوستها</h1>
|
|||
|
|
<p class="sub">فایلهای پیوستشده به یادداشتها</p>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
<p v-if="error" class="error">{{ error }}</p>
|
|||
|
|
<p v-else-if="loading && !items.length" class="muted">در حال بارگذاری…</p>
|
|||
|
|
<p v-else-if="!items.length" class="muted">پیوستی ثبت نشده.</p>
|
|||
|
|
<div v-else class="table-wrap">
|
|||
|
|
<table>
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>نام فایل</th>
|
|||
|
|
<th>یادداشت</th>
|
|||
|
|
<th>حجم</th>
|
|||
|
|
<th>تاریخ</th>
|
|||
|
|
<th></th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
<tr v-for="row in items" :key="`${row.noteId}-${row.id}`">
|
|||
|
|
<td>{{ row.fileName }}</td>
|
|||
|
|
<td>
|
|||
|
|
<RouterLink :to="{ name: 'notes' }">
|
|||
|
|
{{ row.noteTitle }}
|
|||
|
|
</RouterLink>
|
|||
|
|
</td>
|
|||
|
|
<td>{{ sizeLabel(row.sizeBytes) }}</td>
|
|||
|
|
<td>{{ formatJalaliNumeric(new Date(row.createdAt)) }}</td>
|
|||
|
|
<td>
|
|||
|
|
<button type="button" @click="download(row)">دانلود</button>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.page { display: grid; gap: 1rem; }
|
|||
|
|
h1 { margin: 0; font-size: 1.25rem; }
|
|||
|
|
.sub, .muted { margin: 0.25rem 0 0; color: var(--muted); font-size: 0.88rem; }
|
|||
|
|
.error { color: var(--danger, #dc2626); font-weight: 600; }
|
|||
|
|
.table-wrap { overflow: auto; background: #fff; border: 1px solid var(--line); border-radius: var(--radius); }
|
|||
|
|
table { width: 100%; border-collapse: collapse; min-width: 640px; }
|
|||
|
|
th, td { padding: 0.7rem 0.65rem; border-bottom: 1px solid #eef2f7; text-align: right; }
|
|||
|
|
th { background: #f8fafc; color: #64748b; font-size: 0.82rem; }
|
|||
|
|
a { color: #0f766e; font-weight: 700; }
|
|||
|
|
button {
|
|||
|
|
min-height: var(--control-h); border: 1px solid var(--line); border-radius: var(--radius);
|
|||
|
|
padding: 0 0.75rem; background: #fff; cursor: pointer; font: inherit;
|
|||
|
|
}
|
|||
|
|
</style>
|