Skip to content

Commit

Permalink
feat(admin): time
Browse files Browse the repository at this point in the history
Signed-off-by: ZTL-UwU <zhangtianli2006@163.com>
  • Loading branch information
ZTL-UwU committed Nov 16, 2024
1 parent 12ee81d commit 1712503
Show file tree
Hide file tree
Showing 61 changed files with 1,453 additions and 378 deletions.
18 changes: 18 additions & 0 deletions app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,26 @@
</template>

<script setup lang="ts">
import 'v-calendar/style.css';
useHeadSafe({
titleTemplate: (title?: string) => !title ? 'the1068fm 点歌系统' : `${title} | the1068fm 点歌系统`,
meta: [{ name: 'description', content: 'the1068fm 点歌系统 Made by COSMO.' }],
});
</script>

<style>
/* List animation */
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
width: 100%;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
80 changes: 80 additions & 0 deletions app/components/TimeAvailability.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<template>
<div>
<Card
v-if="isCard"
:class="`flex flex-col justify-center items-start shadow p-0 pt-1 pb-2 ${stateColor.from[status]} from-[-10%] ${stateColor.via[status]} via-30% to-white to-80%`" style="background: linear-gradient(310deg, var(--tw-gradient-stops))"
>
<CardHeader :class="`pt-1 pb-0 text-3xl font-bold ${stateColor.text[status]}`">
{{ stateText[status] }}
</CardHeader>
<CardContent class="pt-0 pb-0">
<span class="text-md">目前投稿状态</span>
</CardContent>
</Card>
<div v-else>
<CardContent class="flex flex-row p-2 items-center">
<div
class="h-3.5 w-3.5 rounded-full border-2" :class="[stateColor.bg[state], stateColor.border[state]]"
/>
<span class="ml-2 text-sm">目前投稿状态</span>
<span class="font-bold ml-2 text-sm" :class="stateColor.text[state]">{{ stateText[state] }}</span>
</CardContent>
</div>
</div>
</template>

<script setup lang="ts">
type TState = 'unknown' | 'can' | 'cannot';
const {
isCard = false,
status = 'unknown',
} = defineProps<{
showButton?: boolean;
isCard?: boolean;
status?: TState;
}>();
const { $trpc } = useNuxtApp();
const { data, suspense } = useQuery({
queryFn: () => $trpc.time.currently.query(),
queryKey: ['time.currently'],
});
await suspense();
const state = computed<TState>(() => data.value ? 'can' : 'cannot');
const stateText = {
unknown: '未知',
can: '开放',
cannot: '关闭',
};
const stateColor = {
bg: {
unknown: 'bg-slate-300',
can: 'bg-green-300',
cannot: 'bg-red-300',
},
border: {
unknown: 'border-slate-500',
can: 'border-green-500',
cannot: 'border-red-500',
},
text: {
unknown: 'text-slate-500',
can: 'text-green-500',
cannot: 'text-red-500',
},
from: {
unknown: 'from-slate-300',
can: 'from-green-300',
cannot: 'from-red-300',
},
via: {
unknown: 'via-slate-100',
can: 'via-green-100',
cannot: 'via-red-100',
},
};
</script>
22 changes: 22 additions & 0 deletions app/components/admin/time/DayPicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<div class="flex flex-row gap-2">
<Button type="button" variant="outline" size="icon" @click="handleChange(value.setDate(value.getDate() - 1))">
<Icon name="lucide:minus" />
</Button>
<div class="text-2xl font-bold self-center">
{{ `${dayString[value.getDay()]}` }}
</div>
<Button type="button" variant="outline" size="icon" @click="handleChange(value.setDate(value.getDate() + 1))">
<Icon name="lucide:plus" />
</Button>
</div>
</template>

<script setup lang="ts">
defineProps<{
value: Date;
handleChange: any;
}>();
const dayString = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
</script>
64 changes: 64 additions & 0 deletions app/components/admin/time/TimeCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<template>
<div
variant="outline"
class="h-auto p-4 w-full border rounded-lg shadow-sm hover:bg-muted transition-colors cursor-pointer"
:class="{ 'bg-muted/60': selected }"
>
<div class="text-secondary-foreground flex flex-row justify-between">
<Badge variant="secondary">
{{ time.name }}
</Badge>
<Switch :checked="time.isActive" @update:checked="mutate({ id: time.id, isActive: !time.isActive })" />
</div>
<div class="flex flex-row mb-6 mt-4">
<span class="px-5 lg:px-10 pt-2">
<div v-if="time.repeats" class="text-2xl font-bold text-center">
{{ `${dayString[time.startAt.getDay()]}` }}
</div>
<div v-else class="text-2xl font-bold text-center">
{{ `${time.startAt.getMonth() + 1}-${time.startAt.getDate()}` }}
</div>
<div class="text-lg text-center">
{{ `${time.startAt.getHours().toString().padStart(2, '0')}:${time.startAt.getMinutes().toString().padStart(2, '0')}` }}
</div>
</span>
<span class="flex-grow flex">
<div class="w-[50px] h-[2px] bg-slate-200 mx-auto self-center rounded-full" />
</span>
<span class="px-5 lg:px-10 pt-2">
<div v-if="time.repeats" class="text-2xl font-bold text-center">
{{ `${dayString[time.endAt.getDay()]}` }}
</div>
<div v-else class="text-2xl font-bold text-center">
{{ `${time.endAt.getMonth() + 1}-${time.endAt.getDate()}` }}
</div>
<div class="text-lg text-center">
{{ `${time.endAt.getHours().toString().padStart(2, '0')}:${time.endAt.getMinutes().toString().padStart(2, '0')}` }}
</div>
</span>
</div>
</div>
</template>

<script setup lang="ts">
import type { RouterOutput } from '~~/types';
const { selected = false } = defineProps<{
selected?: boolean;
time: RouterOutput['time']['list'][0];
}>();
const { $trpc } = useNuxtApp();
const dayString = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const queryClient = useQueryClient();
const { mutate } = useMutation({
mutationFn: $trpc.time.modifyActive.mutate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['time.list'] });
queryClient.invalidateQueries({ queryKey: ['time.currently'] });
},
onError: err => useErrorHandler(err),
});
</script>
144 changes: 144 additions & 0 deletions app/components/admin/time/TimeCreateForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<template>
<CardHeader>
<CardTitle>
创建开放时间
</CardTitle>
</CardHeader>
<CardContent>
<form class="flex flex-col gap-4" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>名称</FormLabel>
<FormControl>
<Input type="text" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>

<FormField v-slot="{ value, handleChange }" name="repeats">
<FormItem>
<FormLabel class="block">
每周重复
</FormLabel>
<FormControl>
<Switch :checked="value" @update:checked="handleChange" />
</FormControl>
<FormMessage />
</FormItem>
</FormField>

<div v-show="!values.repeats" class="grid grid-cols-2 gap-10">
<FormField v-slot="{ handleChange, value }" name="startAt">
<FormItem>
<FormLabel class="block">
开始时间
</FormLabel>
<DatePicker
borderless
:model-value="value" mode="dateTime" color="gray" locale="zh" trim-weeks
is-required is24hr class="border rounded-lg shadow-sm"
expanded
@update:model-value="handleChange"
/>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ handleChange, value }" name="endAt">
<FormItem>
<FormLabel class="block">
结束时间
</FormLabel>
<DatePicker
borderless
:model-value="value" mode="dateTime" color="gray" locale="zh" trim-weeks
is-required is24hr class="border rounded-lg shadow-sm"
expanded
@update:model-value="handleChange"
/>
<FormMessage />
</FormItem>
</FormField>
</div>
<div v-show="values.repeats" class="grid grid-cols-2 gap-10">
<FormField v-slot="{ handleChange, value }" name="startAt">
<FormItem>
<FormLabel class="block">
开始时间
</FormLabel>
<FormControl>
<AdminTimeDayPicker :handle-change="handleChange" :value="value" />
<DatePicker
:model-value="value" mode="time" color="gray" locale="zh" hide-time-header
is-required is24hr style="border: none !important" @update:model-value="handleChange"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>

<FormField v-slot="{ handleChange, value }" name="endAt">
<FormItem>
<FormLabel class="block">
结束时间
</FormLabel>
<FormControl>
<AdminTimeDayPicker :handle-change="handleChange" :value="value" />
<DatePicker
:model-value="value" mode="time" color="gray" locale="zh" hide-time-header
is-required is24hr style="border: none !important" @update:model-value="handleChange"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</div>

<div class="mt-4">
<Button type="submit" :disabled="isPending">
<Icon v-if="isPending" name="lucide:loader-circle" class="mr-2 animate-spin" />
创建
</Button>
</div>
</form>
</CardContent>
</template>

<script setup lang="ts">
import * as z from 'zod';
import { DatePicker } from 'v-calendar';
const { $trpc } = useNuxtApp();
const formSchema = toTypedSchema(z.object({
name: z.string({ required_error: '名称长度至少为1' }).max(50, '名称长度最大为50'),
repeats: z.boolean(),
startAt: z.date(),
endAt: z.date(),
}));
const { handleSubmit, resetForm, values } = useForm({
validationSchema: formSchema,
initialValues: {
repeats: false,
startAt: new Date(),
endAt: new Date(),
},
});
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: $trpc.time.create.mutate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['time.list'] });
queryClient.invalidateQueries({ queryKey: ['time.currently'] });
toast.success('创建成功');
resetForm();
},
onError: err => useErrorHandler(err),
});
const onSubmit = handleSubmit(async (values) => {
mutate(values);
});
</script>
Loading

0 comments on commit 1712503

Please sign in to comment.