feat: Add CardPopover component for video actions and integrate EllipsisVerticalIcon
This commit is contained in:
32
components.d.ts
vendored
32
components.d.ts
vendored
@@ -17,7 +17,9 @@ declare module 'vue' {
|
|||||||
ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
||||||
ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
||||||
Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||||
|
Button: typeof import('primevue/button')['default']
|
||||||
Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
||||||
|
Checkbox: typeof import('primevue/checkbox')['default']
|
||||||
CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
|
CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
|
||||||
CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
|
CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
|
||||||
CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
||||||
@@ -26,23 +28,37 @@ declare module 'vue' {
|
|||||||
CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
||||||
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
||||||
DashboardNav: typeof import('./src/components/DashboardNav.vue')['default']
|
DashboardNav: typeof import('./src/components/DashboardNav.vue')['default']
|
||||||
|
EllipsisVerticalIcon: typeof import('./src/components/icons/EllipsisVerticalIcon.vue')['default']
|
||||||
EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
|
EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
|
||||||
|
FloatLabel: typeof import('primevue/floatlabel')['default']
|
||||||
GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
|
GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
|
||||||
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||||
Home: typeof import('./src/components/icons/Home.vue')['default']
|
Home: typeof import('./src/components/icons/Home.vue')['default']
|
||||||
|
IconField: typeof import('primevue/iconfield')['default']
|
||||||
InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
||||||
|
InputGroup: typeof import('primevue/inputgroup')['default']
|
||||||
|
InputGroupAddon: typeof import('primevue/inputgroupaddon')['default']
|
||||||
|
InputIcon: typeof import('primevue/inputicon')['default']
|
||||||
InputText: typeof import('primevue/inputtext')['default']
|
InputText: typeof import('primevue/inputtext')['default']
|
||||||
Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||||
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||||
|
Menu: typeof import('primevue/menu')['default']
|
||||||
|
Message: typeof import('primevue/message')['default']
|
||||||
NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
||||||
PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
||||||
|
Paginator: typeof import('primevue/paginator')['default']
|
||||||
PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
|
PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
|
||||||
|
Password: typeof import('primevue/password')['default']
|
||||||
PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default']
|
PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default']
|
||||||
|
Popover: typeof import('primevue/popover')['default']
|
||||||
RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
Select: typeof import('primevue/select')['default']
|
||||||
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
||||||
|
Skeleton: typeof import('primevue/skeleton')['default']
|
||||||
StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
||||||
|
Tag: typeof import('primevue/tag')['default']
|
||||||
TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
||||||
TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
||||||
Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||||
@@ -60,7 +76,9 @@ declare global {
|
|||||||
const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
||||||
const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
||||||
const Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
const Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||||
|
const Button: typeof import('primevue/button')['default']
|
||||||
const Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
const Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
||||||
|
const Checkbox: typeof import('primevue/checkbox')['default']
|
||||||
const CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
|
const CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
|
||||||
const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
|
const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
|
||||||
const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
||||||
@@ -69,23 +87,37 @@ declare global {
|
|||||||
const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
||||||
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
||||||
const DashboardNav: typeof import('./src/components/DashboardNav.vue')['default']
|
const DashboardNav: typeof import('./src/components/DashboardNav.vue')['default']
|
||||||
|
const EllipsisVerticalIcon: typeof import('./src/components/icons/EllipsisVerticalIcon.vue')['default']
|
||||||
const EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
|
const EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
|
||||||
|
const FloatLabel: typeof import('primevue/floatlabel')['default']
|
||||||
const GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
|
const GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
|
||||||
const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||||
const Home: typeof import('./src/components/icons/Home.vue')['default']
|
const Home: typeof import('./src/components/icons/Home.vue')['default']
|
||||||
|
const IconField: typeof import('primevue/iconfield')['default']
|
||||||
const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
||||||
|
const InputGroup: typeof import('primevue/inputgroup')['default']
|
||||||
|
const InputGroupAddon: typeof import('primevue/inputgroupaddon')['default']
|
||||||
|
const InputIcon: typeof import('primevue/inputicon')['default']
|
||||||
const InputText: typeof import('primevue/inputtext')['default']
|
const InputText: typeof import('primevue/inputtext')['default']
|
||||||
const Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
const Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||||
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||||
|
const Menu: typeof import('primevue/menu')['default']
|
||||||
|
const Message: typeof import('primevue/message')['default']
|
||||||
const NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
const NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
||||||
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
||||||
|
const Paginator: typeof import('primevue/paginator')['default']
|
||||||
const PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
|
const PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
|
||||||
|
const Password: typeof import('primevue/password')['default']
|
||||||
const PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default']
|
const PencilIcon: typeof import('./src/components/icons/PencilIcon.vue')['default']
|
||||||
|
const Popover: typeof import('primevue/popover')['default']
|
||||||
const RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
const RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
||||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
const RouterView: typeof import('vue-router')['RouterView']
|
const RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
const Select: typeof import('primevue/select')['default']
|
||||||
const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
||||||
|
const Skeleton: typeof import('primevue/skeleton')['default']
|
||||||
const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
||||||
|
const Tag: typeof import('primevue/tag')['default']
|
||||||
const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
||||||
const TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
const TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
||||||
const Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
const Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||||
|
|||||||
5
src/components/icons/EllipsisVerticalIcon.vue
Normal file
5
src/components/icons/EllipsisVerticalIcon.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 512" fill="currentColor">
|
||||||
|
<path d="M64 360a56 56 0 1 0 0 112 56 56 0 1 0 0-112zm0-160a56 56 0 1 0 0 112 56 56 0 1 0 0-112zM120 96A56 56 0 1 0 8 96a56 56 0 1 0 112 0z"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
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 { formatDate, formatDuration, getStatusSeverity } from '@/lib/utils';
|
||||||
import Card from 'primevue/card';
|
import Card from 'primevue/card';
|
||||||
import Checkbox from 'primevue/checkbox';
|
import Checkbox from 'primevue/checkbox';
|
||||||
|
import CardPopover from './CardPopover.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
videos: ModelVideo[];
|
videos: ModelVideo[];
|
||||||
@@ -30,7 +31,7 @@ const emit = defineEmits<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Card v-for="video in videos" :key="video.id" v-else
|
<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) }">
|
:class="{ '!border-primary ring-2 ring-primary': selectedVideos.some(v => v.id === video.id) }">
|
||||||
|
|
||||||
<template #header>
|
<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 class="text-xs text-gray-500 mb-3 line-clamp-1 h-4">{{ video.description || 'No description' }}
|
||||||
</p>
|
</p>
|
||||||
|
<div class="text-xs text-gray-400 mt-auto">
|
||||||
<div class="mt-auto flex items-center justify-between">
|
{{ formatDate(video.created_at) }}
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user