feat: Add CardPopover component for video actions and integrate EllipsisVerticalIcon
This commit is contained in:
139
src/routes/video/components/CardPopover.vue
Normal file
139
src/routes/video/components/CardPopover.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
|
||||
<template>
|
||||
<div class="card flex justify-center">
|
||||
<Button type="button" class="!border-none" @click="toggle" severity="secondary" variant="text" aria-haspopup="true" aria-controls="overlay_menu">
|
||||
<EllipsisVerticalIcon class="w-4 h-4 text-gray-500" />
|
||||
</Button>
|
||||
<Menu ref="menu" id="overlay_menu" :model="items as any" :popup="true" class="min-w-[160px]">
|
||||
<template #item="{ item, props }">
|
||||
<router-link v-if="(item as any).route" v-bind="props.action" :to="(item as any).route" class="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded cursor-pointer">
|
||||
<component :is="(item as any).icon" class="w-4 h-4" :class="(item as any).iconClass" />
|
||||
<span :class="(item as any).labelClass">{{ item.label }}</span>
|
||||
</router-link>
|
||||
<a v-else-if="!(item as any).separator" v-bind="props.action" @click="(item as any).command" class="flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded cursor-pointer">
|
||||
<component :is="(item as any).icon" class="w-4 h-4" :class="(item as any).iconClass" />
|
||||
<span :class="(item as any).labelClass">{{ item.label }}</span>
|
||||
</a>
|
||||
</template>
|
||||
</Menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DefineComponent } from "vue";
|
||||
import ArrowDownTray from "@/components/icons/ArrowDownTray.vue";
|
||||
import LinkIcon from "@/components/icons/LinkIcon.vue";
|
||||
import PencilIcon from "@/components/icons/PencilIcon.vue";
|
||||
import TrashIcon from "@/components/icons/TrashIcon.vue";
|
||||
import EllipsisVerticalIcon from "@/components/icons/EllipsisVerticalIcon.vue";
|
||||
import type { ModelVideo } from '@/api/client';
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import Menu from "primevue/menu";
|
||||
import { computed, ref, shallowRef } from "vue";
|
||||
import type { RouteLocationRaw } from "vue-router";
|
||||
|
||||
const props = defineProps<{
|
||||
video: ModelVideo
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'delete'): void;
|
||||
}>();
|
||||
|
||||
const toast = useToast();
|
||||
const menu = ref<InstanceType<typeof Menu>>();
|
||||
|
||||
const videoUrl = computed(() => {
|
||||
return `${window.location.origin}/videos/${props.video.id}`;
|
||||
});
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(videoUrl.value);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Thành công',
|
||||
detail: 'Đã sao chép link video',
|
||||
life: 3000
|
||||
});
|
||||
} catch {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Lỗi',
|
||||
detail: 'Không thể sao chép link',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (props.video.id) {
|
||||
const link = document.createElement('a');
|
||||
link.href = props.video.hls_path || videoUrl.value;
|
||||
link.download = props.video.title || 'video';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Thành công',
|
||||
detail: 'Đang tải xuống video...',
|
||||
life: 3000
|
||||
});
|
||||
} else {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Lỗi',
|
||||
detail: 'Không tìm thấy file video',
|
||||
life: 3000
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
emit('delete');
|
||||
};
|
||||
|
||||
interface CustomMenuItem {
|
||||
label?: string;
|
||||
icon?: DefineComponent<{}, {}, any>;
|
||||
iconClass?: string;
|
||||
labelClass?: string;
|
||||
separator?: boolean;
|
||||
route?: RouteLocationRaw;
|
||||
command?: () => void;
|
||||
}
|
||||
|
||||
const items = shallowRef<CustomMenuItem[]>([
|
||||
{
|
||||
label: 'Tải xuống',
|
||||
icon: ArrowDownTray,
|
||||
command: handleDownload
|
||||
},
|
||||
{
|
||||
label: 'Sao chép link',
|
||||
icon: LinkIcon,
|
||||
command: handleCopyLink
|
||||
},
|
||||
{
|
||||
separator: true
|
||||
},
|
||||
{
|
||||
label: 'Chỉnh sửa',
|
||||
icon: PencilIcon,
|
||||
route: { name: 'video-detail', params: { id: props.video.id } }
|
||||
},
|
||||
{
|
||||
label: 'Xóa',
|
||||
icon: TrashIcon,
|
||||
iconClass: 'text-red-500',
|
||||
labelClass: 'text-red-500',
|
||||
command: handleDelete
|
||||
}
|
||||
]);
|
||||
|
||||
const toggle = (event: Event) => {
|
||||
menu.value?.toggle(event);
|
||||
};
|
||||
</script>
|
||||
@@ -3,6 +3,7 @@ import type { ModelVideo } from '@/api/client';
|
||||
import { formatDate, formatDuration, getStatusSeverity } from '@/lib/utils';
|
||||
import Card from 'primevue/card';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import CardPopover from './CardPopover.vue';
|
||||
|
||||
defineProps<{
|
||||
videos: ModelVideo[];
|
||||
@@ -30,7 +31,7 @@ const emit = defineEmits<{
|
||||
</div>
|
||||
</div>
|
||||
<Card v-for="video in videos" :key="video.id" v-else
|
||||
class="overflow-hidden shadow-sm hover:shadow-md transition-shadow group relative border border-gray-200"
|
||||
class="overflow-hidden transition group relative border-2 border-gray-200 !shadow-none"
|
||||
:class="{ '!border-primary ring-2 ring-primary': selectedVideos.some(v => v.id === video.id) }">
|
||||
|
||||
<template #header>
|
||||
@@ -74,16 +75,18 @@ const emit = defineEmits<{
|
||||
|
||||
<p class="text-xs text-gray-500 mb-3 line-clamp-1 h-4">{{ video.description || 'No description' }}
|
||||
</p>
|
||||
|
||||
<div class="mt-auto flex items-center justify-between">
|
||||
<Tag :value="video.status" :severity="getStatusSeverity(video.status)"
|
||||
class="capitalize px-2 py-0.5 text-xs" />
|
||||
<div class="text-[10px] text-gray-400">
|
||||
{{ formatDate(video.created_at) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-auto">
|
||||
{{ formatDate(video.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="mt-auto flex items-center justify-between">
|
||||
<Tag :value="video.status" :severity="getStatusSeverity(video.status)"
|
||||
class="capitalize px-2 py-0.5 text-xs" />
|
||||
<CardPopover :video="video" @delete="emit('delete', video.id || '')"/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user