Skip to content

Commit

Permalink
fully implemented file upload
Browse files Browse the repository at this point in the history
  • Loading branch information
KishinZW committed Aug 3, 2024
1 parent b761b3d commit 130f5b1
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 50 deletions.
39 changes: 39 additions & 0 deletions components/ui/progress/Progress.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue'
import {
ProgressIndicator,
ProgressRoot,
type ProgressRootProps,
} from 'radix-vue'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<ProgressRootProps & { class?: HTMLAttributes['class'] }>(),
{
modelValue: 0,
},
)
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>

<template>
<ProgressRoot
v-bind="delegatedProps"
:class="
cn(
'relative h-2 w-full overflow-hidden rounded-full bg-primary/20',
props.class,
)
"
>
<ProgressIndicator
class="h-full w-full flex-1 bg-primary transition-all"
:style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%);`"
/>
</ProgressRoot>
</template>
1 change: 1 addition & 0 deletions components/ui/progress/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Progress } from './Progress.vue'
112 changes: 74 additions & 38 deletions pages/content/create.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<template>
<Title>登录</Title>
<Title>上传内容</Title>
<div class="w-full lg:grid h-full">
<div class="flex items-center justify-center">
<div class="mx-auto grid w-[400px] gap-6">
<Card>
<CardHeader>
<CardTitle class="text-2xl">
上传新内容
上传内容
</CardTitle>
<CardDescription>
上传图片或视频以显示到食堂显示屏上
Expand All @@ -24,21 +24,22 @@
</div>
<div class="grid gap-2">
<Label for="category">类型</Label>
<Popover v-model:open="open">
<Popover v-model:open="unfoldCheckbox">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="open"
class="w-24"
:aria-expanded="unfoldCheckbox"
class="flex w-full justify-between"
>
选择类型
<p>
{{ checkedCategory ? checkedCategory : '选择类型' }}
</p>
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-2">
<Command>
<CommandInput class="h-9" placeholder="Search framework..." />
<CommandEmpty>请选择类型</CommandEmpty>
<CommandList>
<CommandGroup>
Expand All @@ -47,9 +48,11 @@
:key="pool.id"
:value="pool.id"
@select="(ev: any) => {
if (typeof ev.detail.value === 'number')
form.categoryId = ev.detail.value
open = false;
if (typeof ev.detail.value === 'number') {
form.categoryId = ev.detail.value;
checkedCategory = pool.category;
}
unfoldCheckbox = false;
}"
>
{{ pool.category }}
Expand Down Expand Up @@ -79,7 +82,7 @@
<Label for="lifespan">有效期(天)</Label>
<Input
id="lifespan"
v-model="form.lifespan"
v-model="lifespan"
type="number"
max="180"
min="0"
Expand All @@ -98,11 +101,12 @@
</div>
</div>
</div>
<Button v-if="!isPending" type="submit" class="w-full" @click="createContent">
登录
<Progress v-if="isUploading" v-model="progress" />
<Button v-if="!isUploading" type="submit" class="w-full" @click="createContent">
创建内容
</Button>
<Button v-if="isPending" type="submit" class="w-full" disabled>
<Loader2 v-if="isPending" class="w-4 h-4 mr-2 animate-spin" />
<Button v-if="isUploading" type="submit" class="w-full" disabled>
<Loader2 v-if="isUploading" class="w-4 h-4 mr-2 animate-spin" />
请稍候……
</Button>
</div>
Expand All @@ -121,7 +125,6 @@ import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
Expand All @@ -130,37 +133,40 @@ import {
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Progress } from '@/components/ui/progress';
import { makeId } from '~/server/trpc/utils/shared';
const { $api } = useNuxtApp();
const userStore = useUserStore();
const form = reactive({
const unfoldCheckbox = ref(false);
const checkedCategory = ref('');
const lifespan = ref(0);
interface Form {
name: string;
ownerId: number;
duration: number;
fileType: string;
S3FileId: string;
lifespan: number;
categoryId: number;
};
const form: Form = reactive({
name: '',
ownerId: 0,
duration: 0,
fileType: '',
S3FileId: '',
lifespan: 0,
state: 'created',
categoryId: 0,
});
const img_types = new Set([
'jfif',
'pjpeg',
'jpeg',
'pjp',
'jpg',
'png',
]);
const video_types = new Set([
'm4v',
'mp4',
]);
const allowed_types = new Set(['video', 'image']);
const { data: categoryList } = useQuery({
queryKey: ['pool.list'],
queryFn: () => $api.pool.list.query(),
});
const { mutate: createMutation, isPending } = useMutation({
const { mutate: createMutation } = useMutation({
mutationFn: $api.content.create.mutate,
onSuccess: () => toast.success('内容创建成功'),
onError: err => useErrorHandler(err),
Expand All @@ -172,23 +178,53 @@ const { files, open: openFileDialog, reset, onChange } = useFileDialog({
});
onChange((filelist: FileList | null) => {
if (filelist) {
const extname = filelist[0].name.split('.')[-1];
if (img_types.has(extname)) {
form.fileType = 'image';
} else if (video_types.has(extname)) {
form.fileType = 'video';
const fileType = filelist[0].type;
if (allowed_types.has(fileType.split('/')[0])) {
form.fileType = fileType;
} else {
reset();
toast.error('只能上传图片或视频');
}
}
});
function createContent() {
const progress = ref(0);
const isUploading = ref(false);
async function createContent() {
if (!files.value) {
toast.error('未选择文件');
return;
}
form.lifespan = form.lifespan * 86400;
form.lifespan = lifespan.value * 86400;
if (userStore.userId) {
form.ownerId = userStore.userId;
form.S3FileId = `${makeId(20)}/user-${userStore.userId}/file-${files.value[0].name}`;
} else {
navigateTo('/login');
return;
}
try {
const uploadURL = await $api.s3.getUploadURL.query({ s3FileId: form.S3FileId });
const file = files.value[0];
if (uploadURL) {
isUploading.value = true;
await axios.put(uploadURL, file.slice(), {
headers: {
'Content-Type': file.type,
},
onUploadProgress: (p) => {
progress.value = Math.floor((p.progress ?? 0) * 100);
},
});
}
} catch (err: any) {
toast.error(`文件上传失败 ${err}`);
isUploading.value = false;
return;
}
createMutation(form);
isUploading.value = false;
};
</script>
3 changes: 3 additions & 0 deletions pages/content/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
<TableHead class="w-64">
内容名称
</TableHead>
<TableHead>
创建者
</TableHead>
<TableHead>
创建时间
</TableHead>
Expand Down
2 changes: 1 addition & 1 deletion server/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const contents = sqliteTable('contents', {
S3FileId: text('s3_file_id').notNull(),
lifespan: integer('lifespan').notNull(), // in seconds
state: text('state', { enum: ['created', 'approved', 'rejected', 'inuse', 'outdated'] }).notNull().default('created'),
categoryId: text('category_id').references(() => pools.id, setNull),
categoryId: integer('category_id').references(() => pools.id, setNull),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
});

Expand Down
12 changes: 2 additions & 10 deletions server/trpc/routers/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,11 @@ const nameZod = z.string()
const idZod = z.number();
const durationZod = z.number()
.max(90, { message: '内容展示时长不能超过90秒' });
const fileTypeZod = z.enum(['image', 'video']);
const fileTypeZod = z.string();
const S3FileIdZod = z.string();
const lifespanZod = z.number()
.max(15_552_000, { message: '内容有效期不能超过180天' });
const stateZod = z.enum([
'created',
'approved',
'rejected',
'inuse',
'outdated',
]);
const categoryIdZod = z.string();
const categoryIdZod = z.number();

export const contentRouter = router({
create: protectedProcedure
Expand All @@ -29,7 +22,6 @@ export const contentRouter = router({
fileType: fileTypeZod,
S3FileId: S3FileIdZod,
lifespan: lifespanZod,
state: stateZod,
categoryId: categoryIdZod,
}))
.mutation(async ({ ctx, input }) => {
Expand Down
2 changes: 2 additions & 0 deletions server/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { deviceRouter } from './device';
import { programRouter } from './program';
import { contentRouter } from './content';
import { poolRouter } from './pool';
import { s3Router } from './s3';

export const appRouter = router({
user: userRouter,
device: deviceRouter,
program: programRouter,
content: contentRouter,
pool: poolRouter,
s3: s3Router,
});

export type AppRouter = typeof appRouter;
2 changes: 1 addition & 1 deletion server/trpc/routers/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const poolRouter = router({
}),

list: protectedProcedure
.use(requireRoles(['admin']))
.use(requireRoles(['admin', 'club']))
.query(async ({ ctx }) => {
return await ctx.poolController.getList();
}),
Expand Down

0 comments on commit 130f5b1

Please sign in to comment.