-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore(script): .env file updating script * chore(sync): i am back home * chore(sync): switching container * chore(sync): i am back home * chore(sync): today's work so far * chore(sync): adopted fix by twc * feat: file upload mostly finished (TODO: http error handling) * feat: using new sidebar * fix: moved bucket into .env * feat: changeset
- Loading branch information
Showing
18 changed files
with
3,037 additions
and
385 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"enspire": minor | ||
--- | ||
|
||
Added club file uploading portal |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
<script setup lang="ts"> | ||
import type { FileCollection } from '@prisma/client' | ||
import type { AllClubs } from '~~/types/api/user/all_clubs' | ||
import Toaster from '@/components/ui/toast/Toaster.vue' | ||
import { useToast } from '@/components/ui/toast/use-toast' | ||
import { toTypedSchema } from '@vee-validate/zod' | ||
import dayjs from 'dayjs' | ||
import { v4 as uuidv4 } from 'uuid' | ||
import { useForm } from 'vee-validate' | ||
import * as z from 'zod' | ||
const props = defineProps({ | ||
club: { | ||
type: String, | ||
required: true, | ||
}, | ||
collection: { | ||
type: String, | ||
required: true, | ||
}, | ||
filetypes: { | ||
type: Array<string>, | ||
required: true, | ||
}, | ||
title: { | ||
type: String, | ||
required: true, | ||
}, | ||
}) | ||
definePageMeta({ | ||
middleware: ['auth'], | ||
}) | ||
function fileTypesPrompt(fileTypes: string[]) { | ||
if (fileTypes.length === 0 || fileTypes.includes('*')) { | ||
return '无文件类型限制' | ||
} | ||
else { | ||
return `上传类型为 ${fileTypes.join(', ').toUpperCase()} 的文件` | ||
} | ||
} | ||
function fileTypesAcceptAttr(fileTypes: string[]) { | ||
if (fileTypes.length === 0 || fileTypes.includes('*')) { | ||
return '*' | ||
} | ||
else { | ||
return fileTypes.map(type => `.${type}`).join(',') | ||
} | ||
} | ||
// Still seems to be buggy | ||
// const formSchema = toTypedSchema(z.object({ | ||
// file: z.custom(v => v, 'File missing'), | ||
// })) | ||
// 滚一边去 | ||
function readFileAsDataURL(file: File) { | ||
return new Promise((resolve, reject) => { | ||
const reader = new FileReader() | ||
reader.onload = () => resolve(reader.result) | ||
reader.onerror = reject | ||
reader.readAsDataURL(file) | ||
}) | ||
} | ||
const form = useForm({}) | ||
const inputKey = ref(uuidv4()) | ||
const submitting = ref(false) | ||
const onSubmit = form.handleSubmit(async (values) => { | ||
submitting.value = true | ||
await $fetch('/api/files/newRecord', { | ||
method: 'POST', | ||
body: { | ||
clubId: Number.parseInt(props.club), | ||
collectionId: props.collection, | ||
fileContent: await readFileAsDataURL(values.file), | ||
rawName: values.file.name, | ||
}, | ||
}) | ||
form.resetForm() | ||
inputKey.value = uuidv4() | ||
await updateClub() | ||
submitting.value = false | ||
}) | ||
const msg = ref('') | ||
const currentClubData = ref(null) | ||
const clubUpdating = ref(false) | ||
async function updateClub() { | ||
if (!props.club) { | ||
msg.value = '请先选择一个社团' | ||
currentClubData.value = undefined | ||
return | ||
} | ||
clubUpdating.value = true | ||
const data = await $fetch('/api/files/clubRecords', { | ||
method: 'POST', | ||
body: { | ||
cludId: Number.parseInt(props.club), | ||
collection: props.collection, | ||
}, | ||
}) | ||
if (data && data.length !== 0) { | ||
msg.value = `最后提交于 ${dayjs(data[0].createdAt).fromNow()}` | ||
currentClubData.value = data[0] | ||
} | ||
else { | ||
msg.value = '尚未提交' | ||
currentClubData.value = undefined | ||
} | ||
clubUpdating.value = false | ||
} | ||
const downloadLink = ref('') | ||
const downloadFilename = ref('') | ||
const dlink: Ref<HTMLElement | null> = ref(null) | ||
const downloading = ref(false) | ||
async function download() { | ||
if (currentClubData.value) { | ||
downloading.value = true | ||
const data = await $fetch('/api/files/download', { | ||
method: 'POST', | ||
body: { | ||
fileId: currentClubData.value.fileId, | ||
}, | ||
}) | ||
// const blob = new Blob([new Uint8Array(Array.from(atob(data), c => c.charCodeAt(0)))]) | ||
// window.open(URL.createObjectURL(blob)) | ||
// window.open(data) | ||
downloadLink.value = data.url | ||
downloadFilename.value = data.name | ||
dlink.value.click() | ||
downloading.value = false | ||
} | ||
downloadLink.value = '' | ||
downloadFilename.value = '' | ||
} | ||
watch( | ||
() => props.club, | ||
async () => { | ||
await updateClub() | ||
}, | ||
) | ||
await updateClub() | ||
</script> | ||
|
||
<template> | ||
<Card class="px-4 py-4"> | ||
<div class="mb-5 text-xl font-bold"> | ||
{{ title }} | ||
</div> | ||
<form class="inline-block" @submit="onSubmit"> | ||
<FormField v-slot="{ componentField }" name="file"> | ||
<FormItem> | ||
<FormControl> | ||
<Input | ||
v-bind="componentField" | ||
:key="inputKey" class="text-foreground" | ||
type="file" | ||
:accept="fileTypesAcceptAttr(filetypes)" | ||
/> | ||
</FormControl> | ||
<FormDescription> | ||
{{ fileTypesPrompt(filetypes) }} | ||
</FormDescription> | ||
<!-- <FormMessage /> --> | ||
</FormItem> | ||
</FormField> | ||
<div class="mt-2"> | ||
<Button type="submit" variant="secondary" :disabled="!form.values.file || submitting || clubUpdating"> | ||
上传 | ||
</Button> | ||
<Button v-if="currentClubData" :disabled="downloading" variant="outline" class="ml-2" type="button" @click="download"> | ||
下载 | ||
</Button> | ||
</div> | ||
</form> | ||
<div v-if="submitting || clubUpdating" class="mt-2"> | ||
<Skeleton class="h-5 w-full" /> | ||
</div> | ||
<div v-else class="mt-2"> | ||
{{ msg }} | ||
</div> | ||
<a ref="dlink" :href="downloadLink" :download="downloadFilename" class="hidden">Download</a> | ||
</Card> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export { default as Input } from '@/components/ui/input/Input.vue' | ||
export { default as Input } from './Input.vue' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
<script setup lang="ts"> | ||
import type { FileCollection } from '@prisma/client' | ||
import type { AllClubs } from '~~/types/api/user/all_clubs' | ||
import ClubFileUpload from '@/components/custom/club-file-upload.vue' | ||
import Toaster from '@/components/ui/toast/Toaster.vue' | ||
import { useToast } from '@/components/ui/toast/use-toast' | ||
import { toTypedSchema } from '@vee-validate/zod' | ||
import { v4 as uuidv4 } from 'uuid' | ||
import { useForm } from 'vee-validate' | ||
import * as z from 'zod' | ||
// ZOD! | ||
const formSchema = toTypedSchema(z.object({ | ||
file: z | ||
.instanceof(FileList) | ||
.refine(file => file?.length === 1, 'File is required.'), | ||
})) | ||
definePageMeta({ | ||
middleware: ['auth'], | ||
}) | ||
useHead({ | ||
title: 'Club Files | Enspire', | ||
}) | ||
const { toast } = useToast() | ||
const { data: collectionsData, suspense: _s1 } = useQuery<FileCollection[]>({ | ||
queryKey: ['/api/files/collections'], | ||
}) | ||
await _s1() // suspense要await | ||
const collectionLoaded = ref(false) | ||
if (collectionsData.value) { | ||
collectionLoaded.value = true | ||
} | ||
else { | ||
toast({ | ||
title: '错误', | ||
description: '获取上传通道信息出错', | ||
}) | ||
} | ||
const { data: clubData, suspense: _s2 } = useQuery<AllClubs>({ | ||
queryKey: ['/api/user/all_clubs'], | ||
}) | ||
await _s2() // suspense要await | ||
const clubLoaded = ref(false) | ||
if (clubData.value) { | ||
clubLoaded.value = true | ||
} | ||
else { | ||
toast({ | ||
title: '错误', | ||
description: '获取社团信息出错', | ||
}) | ||
} | ||
const selectedClub = ref('') | ||
</script> | ||
|
||
<template> | ||
<Select v-model="selectedClub"> | ||
<SelectTrigger class="mb-4 w-full lg:w-72"> | ||
<SelectValue placeholder="选择一个社团" /> | ||
</SelectTrigger> | ||
<SelectContent> | ||
<SelectItem v-for="club in clubData.president" :key="club.id" :value="club.id"> | ||
{{ club.name.zh }} | ||
</SelectItem> | ||
</SelectContent> | ||
</Select> | ||
<div v-if="!collectionLoaded"> | ||
loading | ||
</div> | ||
<div v-if="collectionLoaded" class="grid grid-cols-1 gap-4 lg:grid-cols-3"> | ||
<ClubFileUpload | ||
v-for="collection in collectionsData" | ||
:key="collection.id" | ||
:club="selectedClub" | ||
:collection="collection.id" | ||
:filetypes="collection.fileTypes" | ||
:title="collection.name" | ||
/> | ||
</div> | ||
<Toaster /> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
-- CreateEnum | ||
CREATE TYPE "FormStatus" AS ENUM ('OPEN', 'CLOSED'); | ||
|
||
-- CreateTable | ||
CREATE TABLE "File" ( | ||
"id" TEXT NOT NULL DEFAULT gen_random_uuid(), | ||
"name" TEXT NOT NULL, | ||
"fileId" TEXT NOT NULL, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
|
||
CONSTRAINT "File_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateTable | ||
CREATE TABLE "FileUploadRecord" ( | ||
"id" TEXT NOT NULL DEFAULT gen_random_uuid(), | ||
"clubId" INTEGER NOT NULL, | ||
"fileId" TEXT NOT NULL, | ||
"fileUploadId" TEXT NOT NULL, | ||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
|
||
CONSTRAINT "FileUploadRecord_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateTable | ||
CREATE TABLE "FileUpload" ( | ||
"id" TEXT NOT NULL DEFAULT gen_random_uuid(), | ||
"name" TEXT NOT NULL, | ||
"status" "FormStatus" NOT NULL, | ||
"fileNaming" TEXT NOT NULL, | ||
"fileTypes" TEXT[], | ||
|
||
CONSTRAINT "FileUpload_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "FileUploadRecord_clubId_key" ON "FileUploadRecord"("clubId"); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "FileUploadRecord_fileId_key" ON "FileUploadRecord"("fileId"); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "FileUploadRecord_fileUploadId_key" ON "FileUploadRecord"("fileUploadId"); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "FileUploadRecord" ADD CONSTRAINT "FileUploadRecord_clubId_fkey" FOREIGN KEY ("clubId") REFERENCES "Club"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "FileUploadRecord" ADD CONSTRAINT "FileUploadRecord_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "File"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "FileUploadRecord" ADD CONSTRAINT "FileUploadRecord_fileUploadId_fkey" FOREIGN KEY ("fileUploadId") REFERENCES "FileUpload"("id") ON DELETE RESTRICT ON UPDATE CASCADE; |
5 changes: 5 additions & 0 deletions
5
db/migrations/20241228072009_fix_relation_files/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
-- DropIndex | ||
DROP INDEX "FileUploadRecord_clubId_key"; | ||
|
||
-- DropIndex | ||
DROP INDEX "FileUploadRecord_fileUploadId_key"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
# Please do not edit this file manually | ||
# It should be added in your version-control system (i.e. Git) | ||
# It should be added in your version-control system (e.g., Git) | ||
provider = "postgresql" |
Oops, something went wrong.