main
vue 175 lines 4.89 KB
Raw
1 <template>
2 <div class="flex flex-col gap-4">
3 <div class="flex items-center justify-between gap-4">
4 <Chip v-if="caseFiles.length" :value="loadingFiles ? 'Loading...' : caseFiles.length" label="files" />
5 </div>
6
7 <n-spin :show="loadingFiles">
8 <!-- Files List -->
9 <div v-if="caseFiles.length" class="flex flex-col gap-2">
10 <CardEntity v-for="file in caseFiles" :key="file.id">
11 <template #header-main>{{ file.file_name }}</template>
12 <template #header-extra>{{ formatDate(file.upload_time, dFormats.datetime) }}</template>
13 <template #default>
14 <div class="flex flex-wrap gap-2">
15 <Chip :value="formatBytes(file.file_size ?? 0)" label="size" />
16 <Chip :value="file.content_type" label="type" />
17 </div>
18 </template>
19 <template #footer-extra>
20 <div class="flex flex-wrap items-center justify-end gap-2">
21 <n-button
22 type="primary"
23 secondary
24 size="small"
25 :loading="downloadingFile === file.file_name"
26 @click="downloadFile(caseId, file.file_name)"
27 >
28 <template #icon>
29 <Icon name="carbon:download" />
30 </template>
31 Download
32 </n-button>
33
34 <n-popconfirm to="body" @positive-click="deleteFile(caseId, file.file_name)">
35 <template #trigger>
36 <n-button
37 size="small"
38 :focusable="false"
39 :loading="deletingFile === file.file_name"
40 >
41 <template #icon>
42 <Icon name="carbon:trash-can" />
43 </template>
44 Delete
45 </n-button>
46 </template>
47 Are you sure you want to delete this file?
48 </n-popconfirm>
49 </div>
50 </template>
51 </CardEntity>
52 </div>
53
54 <n-empty v-else description="No files found" class="min-h-50 justify-center" />
55 </n-spin>
56
57 <div class="flex flex-col gap-2">
58 <n-upload v-model:file-list="fileList" :max="1" :disabled="uploadingFile">
59 <n-upload-dragger>
60 <div>
61 <Icon name="carbon:document-add" :size="28" :depth="3" />
62 </div>
63 <div class="font-semibold">Click or drag a file to this area to upload</div>
64 </n-upload-dragger>
65 </n-upload>
66 <div class="flex justify-end">
67 <n-button
68 type="primary"
69 secondary
70 :disabled="!selectedFile"
71 :loading="uploadingFile"
72 @click="uploadFile"
73 >
74 <template #icon>
75 <Icon name="carbon:cloud-upload" />
76 </template>
77 Upload
78 </n-button>
79 </div>
80 </div>
81 </div>
82 </template>
83
84 <script setup lang="ts">
85 import type { UploadFileInfo } from "naive-ui"
86 import type { CaseDataStoreFile } from "@/types/cases"
87 import type { ApiError } from "@/types/common"
88 import { saveAs } from "file-saver"
89 import { NButton, NEmpty, NPopconfirm, NSpin, NUpload, NUploadDragger, useMessage } from "naive-ui"
90 import { computed, onBeforeMount, ref } from "vue"
91 import Api from "@/api"
92 import CardEntity from "@/components/common/cards/CardEntity.vue"
93 import Chip from "@/components/common/Chip.vue"
94 import Icon from "@/components/common/Icon.vue"
95 import { useSettingsStore } from "@/stores/settings"
96 import { getApiErrorMessage } from "@/utils"
97 import { formatBytes, formatDate } from "@/utils/format"
98
99 const { caseId } = defineProps<{
100 caseId: number
101 }>()
102
103 const message = useMessage()
104 const dFormats = useSettingsStore().dateFormat
105
106 const caseFiles = ref<CaseDataStoreFile[]>([])
107 const loadingFiles = ref(false)
108 const downloadingFile = ref<string | null>(null)
109 const deletingFile = ref<string | null>(null)
110 const uploadingFile = ref(false)
111 const fileList = ref<UploadFileInfo[]>([])
112 const selectedFile = computed<File | null>(() => fileList.value?.[0]?.file || null)
113
114 async function loadCaseFiles(caseId: number) {
115 loadingFiles.value = true
116
117 try {
118 const response = await Api.cases.getCaseFiles(caseId)
119 caseFiles.value = response.data.case_data_store
120 } catch (err) {
121 message.error(getApiErrorMessage(err as ApiError))
122 caseFiles.value = []
123 } finally {
124 loadingFiles.value = false
125 }
126 }
127
128 async function downloadFile(caseId: number, fileName: string) {
129 downloadingFile.value = fileName
130
131 try {
132 const blob = await Api.cases.downloadCaseFile(caseId, fileName)
133 saveAs(blob.data, fileName)
134 } catch (err) {
135 message.error(getApiErrorMessage(err as ApiError))
136 } finally {
137 downloadingFile.value = null
138 }
139 }
140
141 async function deleteFile(caseId: number, fileName: string) {
142 deletingFile.value = fileName
143
144 try {
145 await Api.cases.deleteCaseFile(caseId, fileName)
146 loadCaseFiles(caseId)
147 } catch (err) {
148 message.error(getApiErrorMessage(err as ApiError))
149 } finally {
150 deletingFile.value = null
151 }
152 }
153
154 async function uploadFile() {
155 if (!selectedFile.value || !caseId) return
156
157 uploadingFile.value = true
158
159 try {
160 await Api.cases.uploadCaseFile(caseId, selectedFile.value)
161
162 loadCaseFiles(caseId)
163 fileList.value = []
164 } catch (err) {
165 message.error(getApiErrorMessage(err as ApiError))
166 } finally {
167 uploadingFile.value = false
168 }
169 }
170
171 // Lifecycle
172 onBeforeMount(() => {
173 loadCaseFiles(caseId)
174 })
175 </script>