Commit 0e17dfb8 authored by nick zheng's avatar nick zheng

feat: 我的作品

parent 5218f169
......@@ -7,3 +7,13 @@ export function fetchGetTaskList<T>(payload: object) {
export function fetchGetTaskConfig<T>(taskConfigId: string) {
return request.post<T>(`/bizDigitalHumanMemberTaskConfigRest/getById.json?id=${taskConfigId}`)
}
// 根据id获取作品配置
export function fetchGetTaskConfigById<T>(id: number) {
return request.post<T>(`/bizDigitalHumanMemberTaskStatusRest/getById.json?id=${id}`)
}
// 根据id删除作品
export function fetchDeleteTaskConfigById<T>(id: string) {
return request.post<T>(`/bizDigitalHumanMemberTaskStatusRest/deletedById.json?id=${id}`)
}
......@@ -16,4 +16,13 @@ export default [
taskConfigId: route.query.taskConfigId,
}),
},
{
path: '/video-preview/:id?',
name: 'VideoPreview',
meta: {
rank: 1001,
title: '預覽視頻',
},
component: () => import('@/views/video-work/video-preview.vue'),
},
] as RouteRecordRaw[]
......@@ -31,7 +31,7 @@ export default [
title: '我的作品',
rank: 1001,
},
component: () => import('@/views/opus/opus.vue'),
component: () => import('@/views/video-work/video-work-list.vue'),
},
{
path: '/work/draft',
......
/**
* 下载文件到本地
* @param fileUrl 文件路径
* @param fileType 文件类型
*/
export function downloadFile(fileUrl: string, fileType: string = 'image/jpeg') {
const name = fileUrl.substring(fileUrl.lastIndexOf('/'))
const xhr = new XMLHttpRequest()
xhr.open('GET', fileUrl)
xhr.responseType = 'blob'
xhr.send()
xhr.onload = function () {
const fileBlob = xhr.response
// 将blob转成url
const fileUrl = URL.createObjectURL(new Blob([fileBlob], { type: fileType }))
const elA = document.createElement('a')
elA.href = fileUrl
elA.download = name
elA.click()
URL.revokeObjectURL(fileBlob)
}
}
......@@ -186,7 +186,7 @@ function handleGetDraftListUpdatePageSize(pageSize: number) {
<div
v-for="taskTypeItem in taskTypeList"
:key="taskTypeItem.value"
class="!h-[28px] cursor-pointer rounded-full !px-[12px] !leading-[28px] hover:text-[#5b647a]"
class="!h-[28px] cursor-pointer select-none rounded-full !px-[12px] !leading-[28px] hover:text-[#5b647a]"
:class="taskTypeItem.value === currentSelectedTaskType ? 'bg-[#edeef7]' : ''"
@click="handleFilterDraftListByTaskType(taskTypeItem.value)"
>
......
This diff is collapsed.
import { formatDateTime } from '@/utils/date-formatter'
import { DataTableColumns, NEllipsis, NPopconfirm } from 'naive-ui'
import { DataTableColumns, NEllipsis } from 'naive-ui'
import { h } from 'vue'
type TaskType = 'IMAGE_VIDEO' | 'BASE_VIDEO' | 'ADVANCED_VIDEO'
type TaskStateType = 'SUBMIT' | 'GENERATING' | 'SUCCESS' | 'FAILED'
export interface TaskItem {
export interface VideoWorkItem {
id: number
taskCoverUrl: string
baiduTaskId: string
......@@ -15,22 +15,30 @@ export interface TaskItem {
status: TaskStateType
videoUrl: string
duration: number
taskConfigId: number
subtitleFileUrl: null
createTime: null
updateTime: string
modifiedTime: Date
}
export function createOpusColumns({
handleOpusDownload,
handleOpusDeleteRow,
handleOpusExamine,
handleOpusEditing,
export function createVideoWorkTableColumns({
handleVideoWorkDownload,
handleVideoWorkPreview,
handleVideoWorkEdit,
handleVideoWorkDelete,
}: {
handleOpusDownload: (rowData: TaskItem) => void
handleOpusDeleteRow: (rowData: TaskItem) => void
handleOpusExamine: (rowData: TaskItem) => void
handleOpusEditing: (rowData: TaskItem) => void
}): DataTableColumns<TaskItem> {
handleVideoWorkDownload: (rowData: VideoWorkItem) => void
handleVideoWorkPreview: (rowData: VideoWorkItem) => void
handleVideoWorkEdit: (rowData: VideoWorkItem) => void
handleVideoWorkDelete: (rowData: VideoWorkItem) => void
}): DataTableColumns<VideoWorkItem> {
function durationToTime(duration: number) {
const hours = Math.floor(duration / 3600)
const minutes = Math.floor((duration - hours * 3600) / 60)
const remainingSeconds = duration % 60
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`
}
return [
{
type: 'selection',
......@@ -39,19 +47,19 @@ export function createOpusColumns({
{
title: '作品名稱',
key: 'opusName',
width: 320,
width: 290,
render(row) {
return h('div', { class: 'flex items-center' }, [
h(
'div',
{
style: { width: '111px', height: '55px' },
class: 'mr-[10px] flex items-center justify-center overflow-hidden relative',
style: { width: '64px', height: '36px', marginRight: '10px', flexShrink: 0 },
class: 'flex items-center justify-center overflow-hidden relative',
},
[
h('img', {
src: row.taskCoverUrl,
alt: '作品圖',
alt: '作品圖',
class: 'h-full object-cover',
}),
],
......@@ -63,7 +71,7 @@ export function createOpusColumns({
{
title: '狀態',
key: 'state',
width: 150,
width: 110,
render(row) {
const stateMap = {
SUBMIT: { text: '待生成', color: '#BBBBBB' },
......@@ -74,8 +82,13 @@ export function createOpusColumns({
const state = stateMap[row.status] || { text: '', color: '#CCCCCC' } // 默认灰色
return h('div', { class: 'flex items-center ' }, [
h('div', {
class: 'w-[10px] h-[10px] rounded-[25px] mr-18px reading-[22px]',
style: { backgroundColor: state.color },
style: {
backgroundColor: state.color,
width: '10px',
height: '10px',
borderRadius: '50%',
marginRight: '12px',
},
}),
h('span', {}, state.text),
])
......@@ -84,7 +97,7 @@ export function createOpusColumns({
{
title: '類型',
key: 'type',
width: 120,
width: 110,
render(row) {
const typeMap = {
IMAGE_VIDEO: '照片數字人',
......@@ -97,9 +110,9 @@ export function createOpusColumns({
{
title: '視頻時長',
key: 'videoDuration',
width: 120,
width: 100,
render(row) {
return h('span', {}, row.duration ? row.duration : '--')
return h('span', {}, row.duration ? durationToTime(Math.floor(row.duration / 1000)) : '--')
},
},
{
......@@ -120,50 +133,45 @@ export function createOpusColumns({
h(
'span',
{
onClick: () => handleOpusExamine(row),
style: { marginRight: '20px' },
class: 'text-theme-color cursor-pointer hover:opacity-80',
onClick: () => handleVideoWorkPreview(row),
style: {
marginRight: '20px',
color: row.videoUrl ? '' : '#BBB',
cursor: row.videoUrl ? 'pointer' : 'not-allowed',
},
class: 'text-theme-color hover:opacity-80',
},
'查看',
),
h(
'span',
{
onClick: () => handleOpusDownload(row),
style: { marginRight: '20px' },
class: 'text-theme-color cursor-pointer hover:opacity-80',
onClick: () => handleVideoWorkDownload(row),
style: {
marginRight: '20px',
color: row.videoUrl ? '' : '#BBB',
cursor: row.videoUrl ? 'pointer' : 'not-allowed',
},
class: 'text-theme-color hover:opacity-80',
},
'下載',
),
h(
'span',
{
onClick: () => handleOpusEditing(row),
onClick: () => handleVideoWorkEdit(row),
style: { marginRight: '20px' },
class: 'text-theme-color cursor-pointer hover:opacity-80',
},
'繼續編輯',
),
h(
NPopconfirm,
{
onPositiveClick: () => handleOpusDeleteRow(row),
onNegativeClick: () => {
window.$message.info('取消删除')
},
},
'span',
{
trigger: () =>
h(
'span',
{
style: { marginRight: '20px' },
class: 'text-theme-color cursor-pointer hover:opacity-80',
},
'删除',
),
default: () => '確認要刪除這一行嗎?',
onClick: () => handleVideoWorkDelete(row),
class: 'text-theme-color cursor-pointer hover:opacity-80',
},
'删除',
),
])
},
......
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Left } from '@icon-park/vue-next'
import { fetchGetTaskConfigById } from '@/apis/opus'
import { VideoWorkItem } from './columns'
import { downloadFile } from '@/utils/download'
const router = useRouter()
const currentTaskConfig = ref<VideoWorkItem>()
onMounted(() => {
if (!router.currentRoute.value.params.id) {
router.replace({ name: 'Workbench' })
return
}
handleGetDigitalVideoWorkById(Number(router.currentRoute.value.params.id))
})
function handleBack() {
router.back()
}
function handleGetDigitalVideoWorkById(id: number) {
fetchGetTaskConfigById<VideoWorkItem>(id)
.then((res) => {
if (res.code === 0) {
currentTaskConfig.value = res.data
}
})
.catch(() => {
router.replace({ name: 'Workbench' })
})
}
function handleEditTaskConfig() {
router.push({ name: 'Creation', query: { taskConfigId: currentTaskConfig.value?.taskConfigId } })
}
function handleDownloadVideo() {
currentTaskConfig.value?.videoUrl && downloadFile(currentTaskConfig.value?.videoUrl, 'video/mp4')
}
</script>
<template>
<div class="flex h-screen min-w-[1280px] flex-col bg-[#f3f4fb]">
<header class="flex h-14 items-center justify-between bg-white px-4">
<div class="flex cursor-pointer items-center hover:opacity-80" @click="handleBack">
<Left theme="outline" size="18" fill="#333" :stroke-width="3" class="mr-2" />
<span class="font-500">返回</span>
</div>
<div class="flex items-center">
<div class="flex items-center gap-4">
<NButton class="h-[32px]! rounded-md!" @click="handleEditTaskConfig"> 繼續編輯 </NButton>
<NButton class="w-[72px]! h-[32px]! rounded-md!" type="info" @click="handleDownloadVideo"> 下載 </NButton>
</div>
</div>
</header>
<main class="flex flex-1 items-center justify-center">
<video
:src="currentTaskConfig?.videoUrl"
:poster="currentTaskConfig?.taskCoverUrl"
class="h-[538px] w-[902px]"
controls
/>
</main>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, Ref, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useIntervalFn } from '@vueuse/core'
import { Search, CloseOne } from '@icon-park/vue-next'
import { createData } from './opusData'
import { fetchGetTaskList } from '@/apis/opus'
import { createOpusColumns, TaskItem } from './columns'
import useTableScrollY from '@/composables/useTableScrollY'
import { fetchDeleteTaskConfigById, fetchGetTaskList } from '@/apis/opus'
import { PaginationInfo } from '@/components/custom-pagination/custom-pagination.vue'
import { createVideoWorkTableColumns, VideoWorkItem } from './columns'
import { downloadFile } from '@/utils/download'
const router = useRouter()
const { pageContentWrapRef, tableContentY } = useTableScrollY(48 + 28 + 24 + 44 + 48 + 48 + 50)
......@@ -25,72 +29,153 @@ const taskTypeList = [
},
]
const isShowPagination = computed(() => {
return tableContentY.value > 0
})
const currentSelectedTaskType = ref('')
const checkedOpusKeys: Ref<number[]> = ref([])
const opusList = ref(createData())
const checkedVideoWorkIds = ref<number[]>([])
const searchQuery = ref('')
const filteredTemplateData = ref<TaskItem[]>([])
const pagingInfo = ref({
const videoWorkTableLoading = ref(false)
const videoWorkList = ref<VideoWorkItem[]>([])
const pagingInfo = ref<PaginationInfo>({
pageNo: 0,
totalPages: 1,
pageSize: 10,
totalRows: 2,
totalRows: 0,
})
const onCheckedOpusKeysChange = (newKeys: number[]) => {
checkedOpusKeys.value = newKeys
}
const isShowPagination = computed(() => {
return tableContentY.value > 0
})
const isTaskAllSUCCESS = computed(() => {
return videoWorkList.value.every((taskItem) => {
return taskItem.status === 'SUCCESS'
})
})
watch(
() => isTaskAllSUCCESS.value,
(newVal) => {
newVal ? pauseIntervalFn() : resumeIntervalFn()
},
{ deep: true },
)
onMounted(() => {
handleGetDigitalVideoWorkList()
})
const { pause: pauseIntervalFn, resume: resumeIntervalFn } = useIntervalFn(
async () => {
if (videoWorkTableLoading.value) return
const opusActionColumns = createOpusColumns({
handleOpusDeleteRow(rowData) {
window.$message.info(`删除 ${rowData.id}`)
await fetchGetTaskList<VideoWorkItem[]>({
taskType: currentSelectedTaskType.value,
taskName: searchQuery.value,
pagingInfo: pagingInfo.value,
})
},
handleOpusExamine(rowData) {
window.$message.info(`查看 ${rowData.id}`)
2000,
{ immediateCallback: false, immediate: false },
)
const videoWorkActionColumns = createVideoWorkTableColumns({
handleVideoWorkDownload(rowData) {
if (!rowData.videoUrl) {
return
}
downloadFile(rowData.videoUrl, 'video/mp4')
},
handleOpusDownload(rowData) {
window.$message.info(`下载 ${rowData.id}`)
handleVideoWorkPreview(rowData) {
if (!rowData.videoUrl) {
return
}
router.push({
name: 'VideoPreview',
params: {
id: rowData.id,
},
})
},
handleOpusEditing(rowData) {
window.$message.info(`编辑 ${rowData.id}`)
handleVideoWorkEdit(rowData) {
router.push({ name: 'Creation', query: { taskConfigId: rowData.taskConfigId } })
},
handleVideoWorkDelete(rowData) {
window.$dialog.warning({
title: '請確認是否刪除',
content: '刪除後數據不可恢復',
negativeText: '取消',
positiveText: '確認',
onPositiveClick: async () => {
const res = await fetchDeleteTaskConfigById(rowData!.id! + '')
if (res.code === 0) {
window.$message.success('刪除成功')
videoWorkList.value.length === 1 && (pagingInfo.value.pageNo = pagingInfo.value.pageNo - 1)
await handleGetDigitalVideoWorkList()
}
},
onNegativeClick() {},
})
},
})
function handleUpdateCheckedVideoWorkIds(videoWorkIds: number[]) {
checkedVideoWorkIds.value = videoWorkIds
}
function handleFilterVideoWorkListByTaskType(taskType: string) {
currentSelectedTaskType.value = taskType
handleGetDigitalVideoWorkList()
}
const handleMultiSelectDelete = () => {
window.$dialog.warning({
title: '提示',
content: '確認刪除嗎?',
positiveText: '確認',
negativeText: '取消',
onPositiveClick: () => {
opusList.value = opusList.value.filter((item) => !checkedOpusKeys.value.includes(item.key))
checkedOpusKeys.value = []
window.$message.success('删除成功')
},
onNegativeClick: () => {
window.$message.error('取消')
onPositiveClick: async () => {
if (checkedVideoWorkIds.value.length > 0) {
const res = await fetchDeleteTaskConfigById(checkedVideoWorkIds.value.join(','))
if (res.code === 0) {
window.$message.success('删除成功')
await handleGetDigitalVideoWorkList()
}
}
},
onNegativeClick() {},
})
}
function handleSearchVideoWorkList() {
pagingInfo.value.pageNo = 1
handleGetDigitalVideoWorkList()
}
function handleClearSearchQuery() {
searchQuery.value = ''
pagingInfo.value.pageNo = 1
handleGetDigitalVideoWorkList()
}
async function handleGetTaskList() {
const res = await fetchGetTaskList<TaskItem[]>({ pagingInfo: pagingInfo.value })
async function handleGetDigitalVideoWorkList() {
videoWorkTableLoading.value = true
const res = await fetchGetTaskList<VideoWorkItem[]>({
taskType: currentSelectedTaskType.value,
taskName: searchQuery.value,
pagingInfo: pagingInfo.value,
})
if (res.code === 0) {
filteredTemplateData.value = res.data
videoWorkList.value = res.data
pagingInfo.value = res.pagingInfo as PaginationInfo
videoWorkTableLoading.value = false
checkedVideoWorkIds.value = []
}
}
onMounted(() => {
handleGetTaskList()
})
function handleGetTaskListUpdatePageNo(pageNo: number) {
pagingInfo.value.pageNo = pageNo
......@@ -110,8 +195,9 @@ function handleGetTaskListUpdatePageSize(pageSize: number) {
<div
v-for="taskTypeItem in taskTypeList"
:key="taskTypeItem.value"
class="!h-[28px] cursor-pointer rounded-full !px-[12px] !leading-[28px] hover:text-[#5b647a]"
class="!h-[28px] cursor-pointer select-none rounded-full !px-[12px] !leading-[28px] hover:text-[#5b647a]"
:class="taskTypeItem.value === currentSelectedTaskType ? 'bg-[#edeef7]' : ''"
@click="handleFilterVideoWorkListByTaskType(taskTypeItem.value)"
>
{{ taskTypeItem.label }}
</div>
......@@ -120,14 +206,14 @@ function handleGetTaskListUpdatePageSize(pageSize: number) {
<div>
<NButton
class="!rounded-[4px]"
:class="{ '!border-[#2468f2] !bg-[#2468f2] !text-white': checkedOpusKeys.length > 0 }"
:disabled="!checkedOpusKeys.length"
:class="{ '!border-[#2468f2] !bg-[#2468f2] !text-white': checkedVideoWorkIds.length > 0 }"
:disabled="!checkedVideoWorkIds.length"
@click="handleMultiSelectDelete"
>
刪除
</NButton>
<span v-show="checkedOpusKeys.length" class="color-[#999] ml-[8px] text-[12px]">
選擇{{ checkedOpusKeys.length }}條記錄
<span v-show="checkedVideoWorkIds.length" class="color-[#999] ml-[8px] text-[12px]">
選擇{{ checkedVideoWorkIds.length }}條記錄
</span>
</div>
......@@ -139,6 +225,7 @@ function handleGetTaskListUpdatePageSize(pageSize: number) {
type="text"
class="mx-[11px] w-[183px] border-none outline-none"
placeholder="請輸入名稱進行蒐索"
@keyup.enter="handleSearchVideoWorkList"
/>
<CloseOne
v-show="searchQuery.length"
......@@ -148,19 +235,20 @@ function handleGetTaskListUpdatePageSize(pageSize: number) {
class="mr-[5px] cursor-pointer"
@click="handleClearSearchQuery"
/>
<Search theme="outline" size="16" fill="#00000040" class="cursor-pointer" />
<Search theme="outline" size="16" fill="#00000040" class="cursor-pointer" @click="handleSearchVideoWorkList" />
</div>
</div>
<div :style="{ height: tableContentY + 48 + 'px' }">
<NDataTable
:loading="videoWorkTableLoading"
:bordered="false"
:single-line="false"
:columns="opusActionColumns"
:data="filteredTemplateData"
:checked-row-keys="checkedOpusKeys"
:columns="videoWorkActionColumns"
:data="videoWorkList"
:row-key="(row: VideoWorkItem) => row.id"
:max-height="tableContentY"
:scroll-x="1286"
@update:checked-row-keys="onCheckedOpusKeysChange"
:scroll-x="1086"
@update:checked-row-keys="handleUpdateCheckedVideoWorkIds"
>
<template #empty>
<div :style="{ height: tableContentY + 'px' }" class="flex items-center justify-center">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment