Skip to content

Commit fd87202

Browse files
authored
fix: file upload issues & chores (#681)
* fix: fixed linting and typo * fix(minor)!: renamed api endpoints * chore: types
1 parent fc40f32 commit fd87202

File tree

5 files changed

+178
-159
lines changed

5 files changed

+178
-159
lines changed

app/components/custom/club-file-upload.vue

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
<script setup lang="ts">
2-
import type { FileCollection } from '@prisma/client'
3-
import type { AllClubs } from '~~/types/api/user/all_clubs'
4-
import Toaster from '@/components/ui/toast/Toaster.vue'
5-
import { useToast } from '@/components/ui/toast/use-toast'
62
import { toTypedSchema } from '@vee-validate/zod'
73
import dayjs from 'dayjs'
84
import { v4 as uuidv4 } from 'uuid'
@@ -32,6 +28,19 @@ definePageMeta({
3228
middleware: ['auth'],
3329
})
3430
31+
const form = useForm({}) // The form
32+
const inputKey = ref(uuidv4()) // To force update the <Input file>
33+
const currentClubData: Ref<Record<string, any> | null> = ref(null) // Data for the current club's record
34+
const msg = ref('') // Message (time / status) shown at the bottom of the card
35+
36+
const clubUpdating = ref(false) // Flag for club data updating
37+
const submitting = ref(false) // Flag for file submission
38+
const downloading = ref(false) // Flag for downloading file
39+
40+
const dlink: Ref<HTMLElement | null> = ref(null) // The <a> element
41+
const downloadLink = ref('') // The data url to be filled in <a>
42+
const downloadFilename = ref('') // The filename to be filled in <a>
43+
3544
function fileTypesPrompt(fileTypes: string[]) {
3645
if (fileTypes.length === 0 || fileTypes.includes('*')) {
3746
return '无文件类型限制'
@@ -54,7 +63,6 @@ function fileTypesAcceptAttr(fileTypes: string[]) {
5463
// file: z.custom(v => v, 'File missing'),
5564
// }))
5665
57-
// 滚一边去
5866
function readFileAsDataURL(file: File) {
5967
return new Promise((resolve, reject) => {
6068
const reader = new FileReader()
@@ -64,60 +72,51 @@ function readFileAsDataURL(file: File) {
6472
})
6573
}
6674
67-
const form = useForm({})
68-
const inputKey = ref(uuidv4())
69-
const submitting = ref(false)
7075
const onSubmit = form.handleSubmit(async (values) => {
7176
submitting.value = true
72-
await $fetch('/api/files/newRecord', {
77+
const fileName = values.file.name
78+
const status = await $fetch('/api/files/new-record', {
7379
method: 'POST',
7480
body: {
7581
clubId: Number.parseInt(props.club),
7682
collectionId: props.collection,
7783
fileContent: await readFileAsDataURL(values.file),
78-
rawName: values.file.name,
84+
rawName: fileName,
7985
},
8086
})
8187
form.resetForm()
8288
inputKey.value = uuidv4()
83-
await updateClub()
89+
msg.value = (status && status.success) ? `${fileName} (提交成功)` : '提交失败'
8490
submitting.value = false
8591
})
8692
87-
const downloadLink = ref('')
88-
const downloadFilename = ref('')
89-
const msg = ref('')
90-
const currentClubData = ref(null)
91-
const clubUpdating = ref(false)
9293
async function updateClub() {
9394
downloadLink.value = ''
9495
downloadFilename.value = ''
9596
if (!props.club) {
9697
msg.value = '请先选择一个社团'
97-
currentClubData.value = undefined
98+
currentClubData.value = null
9899
return
99100
}
100101
clubUpdating.value = true
101-
const data = await $fetch('/api/files/clubRecords', {
102+
const data = await $fetch('/api/files/club-records', {
102103
method: 'POST',
103104
body: {
104-
cludId: Number.parseInt(props.club),
105+
clubId: Number.parseInt(props.club),
105106
collection: props.collection,
106107
},
107108
})
108-
if (data && data.length !== 0) {
109-
msg.value = `最后提交于 ${dayjs(data[0].createdAt).fromNow()}`
109+
if (data[0] && data.length !== 0) {
110+
msg.value = `${data[0].file.name} (${dayjs(data[0].createdAt).fromNow()})`
110111
currentClubData.value = data[0]
111112
}
112113
else {
113114
msg.value = '尚未提交'
114-
currentClubData.value = undefined
115+
currentClubData.value = null
115116
}
116117
clubUpdating.value = false
117118
}
118119
119-
const dlink: Ref<HTMLElement | null> = ref(null)
120-
const downloading = ref(false)
121120
async function download() {
122121
if (currentClubData.value) {
123122
downloading.value = true
@@ -130,12 +129,14 @@ async function download() {
130129
// const blob = new Blob([new Uint8Array(Array.from(atob(data), c => c.charCodeAt(0)))])
131130
// window.open(URL.createObjectURL(blob))
132131
// window.open(data)
133-
downloadLink.value = data.url
134-
downloadFilename.value = data.name
135-
nextTick(() => {
136-
dlink.value.click()
137-
})
138-
downloading.value = false
132+
if (typeof data.url === 'string' && typeof data.name === 'string') {
133+
downloadLink.value = data.url
134+
downloadFilename.value = data.name
135+
nextTick(() => {
136+
dlink.value!.click()
137+
})
138+
downloading.value = false
139+
}
139140
}
140141
}
141142
@@ -189,7 +190,5 @@ await updateClub()
189190
<a
190191
ref="dlink" :href="downloadLink" :download="downloadFilename" class="hidden"
191192
>Download</a>
192-
<!-- @click="downloadLink = '';
193-
downloadFilename = ''" -->
194193
</Card>
195194
</template>

app/pages/forms/files.vue

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const { toast } = useToast()
2222
const { data: collectionsData, suspense: _s1 } = useQuery<FileCollection[]>({
2323
queryKey: ['/api/files/collections'],
2424
})
25-
await _s1() // suspense要await
25+
await _s1()
2626
2727
const collectionLoaded = ref(false)
2828
if (collectionsData.value) {
@@ -38,7 +38,7 @@ else {
3838
const { data: clubData, suspense: _s2 } = useQuery<AllClubs>({
3939
queryKey: ['/api/user/all_clubs'],
4040
})
41-
await _s2() // suspense要await
41+
await _s2()
4242
4343
const clubLoaded = ref(false)
4444
if (clubData.value) {
@@ -59,16 +59,16 @@ const selectedClub = ref('')
5959
<SelectTrigger class="mb-4 w-full lg:w-72">
6060
<SelectValue placeholder="选择一个社团" />
6161
</SelectTrigger>
62-
<SelectContent>
63-
<SelectItem v-for="club in clubData.president" :key="club.id" :value="club.id">
62+
<SelectContent v-if="clubLoaded && clubData">
63+
<SelectItem v-for="club in clubData.president" :key="club.id" :value="String(club.id)">
6464
{{ club.name.zh }}
6565
</SelectItem>
66+
<SelectItem value="21">
67+
测试组
68+
</SelectItem>
6669
</SelectContent>
6770
</Select>
68-
<div v-if="!collectionLoaded">
69-
loading
70-
</div>
71-
<div v-if="collectionLoaded" class="grid grid-cols-1 gap-4 lg:grid-cols-3">
71+
<div v-if="clubLoaded && collectionLoaded" class="grid grid-cols-1 gap-4 lg:grid-cols-3">
7272
<ClubFileUpload
7373
v-for="collection in collectionsData"
7474
:key="collection.id"
@@ -78,5 +78,8 @@ const selectedClub = ref('')
7878
:title="collection.name"
7979
/>
8080
</div>
81+
<div v-else>
82+
loading
83+
</div>
8184
<Toaster />
8285
</template>

server/api/files/clubRecords.post.ts renamed to server/api/files/club-records.post.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { PrismaClient } from '@prisma/client'
22

3+
interface Body {
4+
clubId: number
5+
collection: string
6+
}
7+
38
const prisma = new PrismaClient()
49

510
export default eventHandler(async (event) => {
@@ -10,12 +15,16 @@ export default eventHandler(async (event) => {
1015
}
1116

1217
return readBody(event)
13-
.then(async (body) => {
18+
.then(async (body: Body) => {
1419
const { clubId, collection } = body
1520
const records = await prisma.fileUploadRecord.findMany({
1621
where: {
17-
clubId,
18-
fileUploadId: collection,
22+
clubId: {
23+
equals: clubId,
24+
},
25+
fileUploadId: {
26+
equals: collection,
27+
},
1928
},
2029
include: {
2130
file: true,

server/api/files/new-record.post.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { PrismaClient } from '@prisma/client'
2+
import { randomUUID } from 'uncrypto'
3+
4+
interface Body {
5+
clubId: number
6+
collectionId: string
7+
fileContent: string
8+
rawName: string
9+
}
10+
11+
const prisma = new PrismaClient()
12+
13+
export default eventHandler(async (event) => {
14+
const { auth } = event.context
15+
16+
if ((auth?.userId) == null) {
17+
setResponseStatus(event, 403)
18+
}
19+
20+
return readBody(event)
21+
.then(async (body: Body) => {
22+
const { clubId, collectionId, fileContent, rawName } = body
23+
// TODO: this should be enabled after the ID mismatch is fixed
24+
// const collectionInfo = await prisma.fileCollection.findFirst({
25+
// where: {
26+
// id: collectionId,
27+
// },
28+
// })
29+
// const clubInfo = await prisma.club.findFirst({
30+
// where: {
31+
// id: clubId,
32+
// },
33+
// })
34+
// const naming = collectionInfo?.fileNaming
35+
// const fileName = naming
36+
// .replaceAll('$id$', clubId)
37+
// .replaceAll('$club$', clubInfo.name.zh)
38+
// .replaceAll('$ext$', rawName.split('.').pop())
39+
// TODO: (cont.) and this should be removed
40+
const fileName = rawName
41+
try {
42+
// An error will be thrown if the club's record does not exist
43+
const existingRecord = await prisma.fileUploadRecord.findFirstOrThrow({
44+
where: {
45+
clubId,
46+
fileUploadId: collectionId,
47+
},
48+
select: {
49+
id: true,
50+
file: true,
51+
},
52+
})
53+
try {
54+
await prisma.fileUploadRecord.update({
55+
where: {
56+
id: existingRecord.id,
57+
},
58+
data: {
59+
createdAt: new Date(),
60+
file: {
61+
update: {
62+
where: {
63+
id: existingRecord.file.id,
64+
},
65+
data: {
66+
name: fileName,
67+
createdAt: new Date(),
68+
},
69+
},
70+
},
71+
},
72+
})
73+
await useStorage('s3').setItemRaw(existingRecord.file.fileId, fileContent)
74+
}
75+
catch (error: any) {
76+
return {
77+
success: false,
78+
error: String(error),
79+
}
80+
}
81+
return {
82+
success: true,
83+
}
84+
}
85+
catch (__error) {
86+
if (__error instanceof Error) {
87+
// Create a new record (the record is not found)
88+
const fileUUID = randomUUID()
89+
try {
90+
await prisma.fileUploadRecord.create({
91+
data: {
92+
club: {
93+
connect: {
94+
id: clubId,
95+
},
96+
},
97+
fileUpload: {
98+
connect: {
99+
id: collectionId,
100+
},
101+
},
102+
file: {
103+
create: {
104+
fileId: fileUUID,
105+
name: fileName,
106+
},
107+
},
108+
},
109+
})
110+
await useStorage('s3').setItemRaw(fileUUID, fileContent)
111+
return {
112+
success: true,
113+
}
114+
}
115+
catch (error) {
116+
return {
117+
success: false,
118+
error: String(error),
119+
}
120+
}
121+
}
122+
}
123+
})
124+
})

0 commit comments

Comments
 (0)