Commit bfc4695a authored by Dazzle Wu's avatar Dazzle Wu

feat: 最近创作和推荐模板联调

parent 6c2fc5cb
import { request } from '@/utils/request' import { request } from '@/utils/request'
// 根据ID获取推荐模板信息
export function fetchDigitalHumanTemplateStatus<T>(id: number) {
return request.post<T>(`/aiDigitalHumanTemplateStatusRest/getDigitalHumanTemplateStatus.json?id=${id}`)
}
// 获取3D数字人形象信息 // 获取3D数字人形象信息
export function fetch3DImageList<T>() { export function fetch3DImageList<T>() {
return request.post<T>('/bizDigitalHumanImageRest/get3DImageList.json') return request.post<T>('/bizDigitalHumanImageRest/get3DImageList.json')
...@@ -25,9 +20,14 @@ export function fetchBackgroundImage<T>() { ...@@ -25,9 +20,14 @@ export function fetchBackgroundImage<T>() {
return request.post<T>('/aiDigitalHumanImageRest/getBackgroundImageList.json') return request.post<T>('/aiDigitalHumanImageRest/getBackgroundImageList.json')
} }
// 模糊查询背景图
export function fetchSearchBackgroundImageList<T>(imageName: string) {
return request.post<T>(`/aiDigitalHumanImageRest/searchBackgroundImageList.json?imageName=${imageName}`)
}
// 上传背景图片 // 上传背景图片
export function uploadImageFile<T>(imageName: string, formData: FormData) { export function uploadImageFile<T>(imageName: string, formData: FormData) {
return request.post<T>(`/baiduDigitalHumanFileRest/uploadImageFile.json?imageName=${imageName}`, formData, { return request.post<T>(`/baiduDigitalHumanFileRest/uploadBackgroundImageFile.json?imageName=${imageName}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }, headers: { 'Content-Type': 'multipart/form-data' },
timeout: 12000, timeout: 12000,
}) })
...@@ -41,7 +41,7 @@ export function deleteBackgroundImageById<T>(id: number) { ...@@ -41,7 +41,7 @@ export function deleteBackgroundImageById<T>(id: number) {
// 根据人物名称分页获取人物信息 // 根据人物名称分页获取人物信息
export function fetchInfoByImageName<T>(imageName: string) { export function fetchInfoByImageName<T>(imageName: string) {
return request.post<T>(`/bizDigitalHumanImageRest/findByImageName.json?imageName=${imageName}`, { return request.post<T>(`/bizDigitalHumanImageRest/findByImageName.json?imageName=${imageName}`, {
pagingInfo: { pageNo: 1, pageSize: 9999 }, pagingInfo: { pageNo: 1, pageSize: 1000 },
}) })
} }
...@@ -67,5 +67,5 @@ export function saveDraftConfig<T>(payload: object) { ...@@ -67,5 +67,5 @@ export function saveDraftConfig<T>(payload: object) {
// 导出视频到我的作品 // 导出视频到我的作品
export function createDigitalHumanVideoTask<T>(payload: object) { export function createDigitalHumanVideoTask<T>(payload: object) {
return request.post<T>('/aiDigitalHumanTaskRest/createDigitalHumanVideoTask.json', payload) return request.post<T>('/aiDigitalHumanTaskRest/createDigitalHumanVideoTask.json', payload, { timeout: 12000 })
} }
import { request } from '@/utils/request'
// 获取模板
export function fetchDigitalHumanTemplateStatusList<T>() {
return request.post<T>(
'/aiDigitalHumanTemplateStatusRest/getDigitalHumanTemplateStatusList.json?pageNo=1&pageSize=1000',
)
}
// 根据ID获取推荐模板信息
export function fetchDigitalHumanTemplateStatus<T>(id: number) {
return request.post<T>(`/aiDigitalHumanTemplateStatusRest/getDigitalHumanTemplateStatus.json?id=${id}`)
}
import { DraftConfig, DriveType, LangType, TaskType } from '@/store/types/creation' import { DraftConfig, LangType } from '@/store/types/creation'
import { DriveType, TaskType } from '@/store/types/template'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
function defaultDigitalCreation(): DraftConfig { function defaultDigitalCreation(): DraftConfig {
...@@ -7,6 +8,7 @@ function defaultDigitalCreation(): DraftConfig { ...@@ -7,6 +8,7 @@ function defaultDigitalCreation(): DraftConfig {
coverUrl: null, coverUrl: null,
draftName: '', draftName: '',
videoName: '', videoName: '',
videoDuration: null,
taskType: TaskType.BASE_VIDEO, taskType: TaskType.BASE_VIDEO,
requestId: null, requestId: null,
inputImageUrl: null, inputImageUrl: null,
...@@ -37,7 +39,7 @@ function defaultDigitalCreation(): DraftConfig { ...@@ -37,7 +39,7 @@ function defaultDigitalCreation(): DraftConfig {
logoUrl: null, logoUrl: null,
bgmUrl: null, bgmUrl: null,
materialUrl: null, materialUrl: null,
pronunciationLanguageL: LangType.CANTONESE, pronunciationLanguage: LangType.CANTONESE,
} }
} }
...@@ -49,6 +51,10 @@ export const useDigitalCreationStore = defineStore('digital-creation-store', { ...@@ -49,6 +51,10 @@ export const useDigitalCreationStore = defineStore('digital-creation-store', {
state: (): DraftConfig => getLocalState(), state: (): DraftConfig => getLocalState(),
actions: { actions: {
setDraftName(draftName: string) {
this.draftName = draftName
},
setFigureId(figureId: string) { setFigureId(figureId: string) {
this.figureId = figureId this.figureId = figureId
}, },
......
import { DriveType, TaskType } from './template'
export enum ImageType { export enum ImageType {
THREE_D = 'THREE_D', THREE_D = 'THREE_D',
TWO_D_BOUTIQUE = 'TWO_D_BOUTIQUE', TWO_D_BOUTIQUE = 'TWO_D_BOUTIQUE',
TWO_D_FEW_SHOT = 'TWO_D_FEW_SHOT', TWO_D_FEW_SHOT = 'TWO_D_FEW_SHOT',
} }
export enum TaskType { export enum BackgroundImageType {
IMAGE_VIDEO = 'IMAGE_VIDEO', PERSON = 'PERSON',
BASE_VIDEO = 'BASE_VIDEO', PUBLIC = 'PUBLIC',
ADVANCED_VIDEO = 'ADVANCED_VIDEO',
}
export enum DriveType {
TEXT = 'TEXT',
VOICE = 'VOICE',
} }
export enum LangType { export enum LangType {
...@@ -31,59 +27,6 @@ export interface AudioConfig { ...@@ -31,59 +27,6 @@ export interface AudioConfig {
voiceType: VoiceType voiceType: VoiceType
} }
export interface DigitalTemplate {
id: number
coverUrl: string | null
demonstrationGifUrl: string | null
demonstrationVideoUrl: string | null
templateType: string
templateName: string
taskType: TaskType
requestId: string | null
inputImageUrl: string | null
driveType: DriveType
text: string
ttsParams: {
person: string | null
speed: string
volume: string
pitch: string
}
inputAudioUrl: string | null
callbackUrl: string | null
figureId: string
videoParams: {
width: number
height: number
transparent: boolean
}
dhParams: {
cameraId: number | null
position: {
x: number
y: number
w: number
h: number
}
}
subtitleParams: {
subtitlePolicy: string
enabled: boolean
}
backgroundImageUrl: string | null
autoAnimoji: boolean
enablePalindrome: boolean
templateId: string | null
title: string | null
logoParams: {
logoUrl: string | null
}
bgmParams: {
bgmUrl: string | null
}
materialUrl: string | null
}
export interface DigitalImageItem { export interface DigitalImageItem {
id: number id: number
imageType: ImageType imageType: ImageType
...@@ -94,7 +37,7 @@ export interface DigitalImageItem { ...@@ -94,7 +37,7 @@ export interface DigitalImageItem {
export interface BackgroundImageItem { export interface BackgroundImageItem {
id: number id: number
imageSource: string imageSource: BackgroundImageType
imageName: string imageName: string
imageUrl: string imageUrl: string
} }
...@@ -125,6 +68,7 @@ export interface DraftConfig { ...@@ -125,6 +68,7 @@ export interface DraftConfig {
coverUrl: string | null coverUrl: string | null
draftName: string draftName: string
videoName: string videoName: string
videoDuration: number | null
taskType: TaskType taskType: TaskType
requestId: string | null requestId: string | null
inputImageUrl: string | null inputImageUrl: string | null
...@@ -155,7 +99,7 @@ export interface DraftConfig { ...@@ -155,7 +99,7 @@ export interface DraftConfig {
logoUrl: string | null logoUrl: string | null
bgmUrl: string | null bgmUrl: string | null
materialUrl: string | null materialUrl: string | null
pronunciationLanguageL: LangType pronunciationLanguage: LangType
memberId?: number memberId?: number
modifiedTime?: string modifiedTime?: string
} }
......
export enum TemplateType {
FINANCIAL_MARKETING = 'FINANCIAL_MARKETING', // 理財營銷
EDUCATION_LEARNING = 'EDUCATION_LEARNING', // 教育學習
FESTIVAL_HOTS_SPOTS = 'FESTIVAL_HOTS_SPOTS', // 節日熱點
}
export enum TaskType {
IMAGE_VIDEO = 'IMAGE_VIDEO',
BASE_VIDEO = 'BASE_VIDEO',
ADVANCED_VIDEO = 'ADVANCED_VIDEO',
}
export enum DriveType {
TEXT = 'TEXT',
VOICE = 'VOICE',
}
export interface DigitalTemplate {
id: number
coverUrl: string
demonstrationGifUrl: string
demonstrationVideoUrl: string
templateType: TemplateType
templateName: string
taskType: TaskType
requestId: string | null
inputImageUrl: string | null
driveType: DriveType
text: string
ttsParams: {
person: string | null
speed: string
volume: string
pitch: string
}
inputAudioUrl: string | null
callbackUrl: string | null
figureId: string
videoParams: {
width: number
height: number
transparent: boolean
}
dhParams: {
cameraId: number | null
position: {
x: number
y: number
w: number
h: number
}
}
subtitleParams: {
subtitlePolicy: string
enabled: boolean
}
backgroundImageUrl: string | null
autoAnimoji: boolean
enablePalindrome: boolean
templateId: string | null
title: string | null
logoParams: {
logoUrl: string | null
}
bgmParams: {
bgmUrl: string | null
}
materialUrl: string | null
}
...@@ -2,18 +2,25 @@ ...@@ -2,18 +2,25 @@
import { import {
deleteBackgroundImageById, deleteBackgroundImageById,
fetchBackgroundImage, fetchBackgroundImage,
fetchInfoByImageName, fetchSearchBackgroundImageList,
uploadImageFile, uploadImageFile,
} from '@/apis/digital-creation' } from '@/apis/digital-creation'
import { useDigitalCreationStore } from '@/store/modules/creation' import { useDigitalCreationStore } from '@/store/modules/creation'
import { BackgroundImageItem } from '@/store/types/creation' import { BackgroundImageItem, BackgroundImageType } from '@/store/types/creation'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
const digitalCreationStore = useDigitalCreationStore() const digitalCreationStore = useDigitalCreationStore()
const imageList = ref<BackgroundImageItem[]>([]) const backgroundImageTypeList = [
{ value: BackgroundImageType.PERSON, label: '我的' },
{ value: BackgroundImageType.PUBLIC, label: '背景庫' },
]
const currentBackgroundImageType = ref(BackgroundImageType.PERSON)
const personBackgroundImageList = ref<BackgroundImageItem[]>([])
const publicBackgroundImageList = ref<BackgroundImageItem[]>([])
const personBackgroundImageListLoaded = ref(Array(personBackgroundImageList.value.length).fill(false))
const publicBackgroundImageListLoaded = ref(Array(publicBackgroundImageList.value.length).fill(false))
const searchName = ref('') const searchName = ref('')
const uploadLoading = ref(false) const uploadLoading = ref(false)
const loaded = ref(Array(imageList.value.length).fill(false))
onMounted(() => { onMounted(() => {
getBackgroundImageList() getBackgroundImageList()
...@@ -22,18 +29,24 @@ onMounted(() => { ...@@ -22,18 +29,24 @@ onMounted(() => {
async function getBackgroundImageList() { async function getBackgroundImageList() {
const res = await fetchBackgroundImage<BackgroundImageItem[]>() const res = await fetchBackgroundImage<BackgroundImageItem[]>()
if (res.code === 0) { if (res.code === 0) {
imageList.value = res.data publicBackgroundImageList.value = res.data.filter((i) => i.imageSource === BackgroundImageType.PUBLIC)
personBackgroundImageList.value = res.data.filter((i) => i.imageSource === BackgroundImageType.PERSON)
} }
} }
function handleUpdateBackgroundImageType(backgroundImageType: BackgroundImageType) {
currentBackgroundImageType.value = backgroundImageType
}
async function handleSearch(value: string) { async function handleSearch(value: string) {
if (!value) { if (!value) {
getBackgroundImageList() getBackgroundImageList()
return return
} }
const res = await fetchInfoByImageName<BackgroundImageItem[]>(value) const res = await fetchSearchBackgroundImageList<BackgroundImageItem[]>(value)
if (res.code === 0) { if (res.code === 0) {
imageList.value = res.data publicBackgroundImageList.value = res.data.filter((i) => i.imageSource === BackgroundImageType.PUBLIC)
personBackgroundImageList.value = res.data.filter((i) => i.imageSource === BackgroundImageType.PERSON)
} }
} }
...@@ -92,7 +105,9 @@ function handleDelete(id: number) { ...@@ -92,7 +105,9 @@ function handleDelete(id: number) {
} }
function onImageLoaded(index: number) { function onImageLoaded(index: number) {
loaded.value[index] = true currentBackgroundImageType.value === BackgroundImageType.PUBLIC
? (publicBackgroundImageListLoaded.value[index] = true)
: (personBackgroundImageListLoaded.value[index] = true)
} }
</script> </script>
...@@ -100,12 +115,25 @@ function onImageLoaded(index: number) { ...@@ -100,12 +115,25 @@ function onImageLoaded(index: number) {
<n-tabs type="line" animated class="h-full"> <n-tabs type="line" animated class="h-full">
<n-tab-pane name="images" tab="圖片" class="h-full"> <n-tab-pane name="images" tab="圖片" class="h-full">
<div class="h-full overflow-y-auto px-4 py-2"> <div class="h-full overflow-y-auto px-4 py-2">
<n-input v-model:value="searchName" round placeholder="搜索" @input="handleSearch"> <div class="mb-4 flex items-center gap-4">
<template #prefix> <n-button-group>
<CustomIcon class="text-lg" icon="mingcute:search-line" /> <n-button
</template> v-for="backgroundImageTypeItem in backgroundImageTypeList"
</n-input> :key="backgroundImageTypeItem.value"
<div class="h-4"></div> :type="currentBackgroundImageType === backgroundImageTypeItem.value ? 'info' : 'default'"
class="text-xs! w-[70px]!"
@click="handleUpdateBackgroundImageType(backgroundImageTypeItem.value)"
>
{{ backgroundImageTypeItem.label }}
</n-button>
</n-button-group>
<n-input v-model:value="searchName" round placeholder="搜索" @input="handleSearch">
<template #prefix>
<CustomIcon class="text-lg" icon="mingcute:search-line" />
</template>
</n-input>
</div>
<n-grid :x-gap="12" :y-gap="12" :cols="3"> <n-grid :x-gap="12" :y-gap="12" :cols="3">
<n-gi> <n-gi>
<n-spin :show="uploadLoading"> <n-spin :show="uploadLoading">
...@@ -119,8 +147,19 @@ function onImageLoaded(index: number) { ...@@ -119,8 +147,19 @@ function onImageLoaded(index: number) {
<template #description>上傳中</template> <template #description>上傳中</template>
</n-spin> </n-spin>
</n-gi> </n-gi>
<n-gi v-for="(image, index) in imageList" :key="index"> <n-gi
<n-spin :show="!loaded[index]"> v-for="(image, index) in currentBackgroundImageType === BackgroundImageType.PUBLIC
? publicBackgroundImageList
: personBackgroundImageList"
:key="index"
>
<n-spin
:show="
currentBackgroundImageType === BackgroundImageType.PUBLIC
? !publicBackgroundImageListLoaded[index]
: !personBackgroundImageListLoaded[index]
"
>
<div <div
class="h-22 w-22 group relative cursor-pointer overflow-hidden rounded-lg border border-2 bg-gray-100" class="h-22 w-22 group relative cursor-pointer overflow-hidden rounded-lg border border-2 bg-gray-100"
:class=" :class="
...@@ -135,7 +174,7 @@ function onImageLoaded(index: number) { ...@@ -135,7 +174,7 @@ function onImageLoaded(index: number) {
{{ image.imageName }} {{ image.imageName }}
</div> </div>
<div <div
v-if="image.id" v-if="currentBackgroundImageType === BackgroundImageType.PERSON"
class="absolute right-1 top-1 hidden h-7 w-7 cursor-pointer items-center justify-center rounded-md bg-black/40 p-1 group-hover:flex" class="absolute right-1 top-1 hidden h-7 w-7 cursor-pointer items-center justify-center rounded-md bg-black/40 p-1 group-hover:flex"
@click.stop="handleDelete(image.id)" @click.stop="handleDelete(image.id)"
> >
......
...@@ -41,8 +41,8 @@ function playAudio() { ...@@ -41,8 +41,8 @@ function playAudio() {
<div class="max-w-32 truncate">{{ value?.name }}</div> <div class="max-w-32 truncate">{{ value?.name }}</div>
<CustomIcon class="cursor-pointer text-lg" icon="mingcute:volume-line" @click.stop.prevent="playAudio" /> <CustomIcon class="cursor-pointer text-lg" icon="mingcute:volume-line" @click.stop.prevent="playAudio" />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-1">
<n-tag v-for="(style, index) in value?.style" :key="index" type="warning" round>{{ style }}</n-tag> <n-tag v-for="(style, index) in value?.style" :key="index" type="warning" size="tiny" round>{{ style }}</n-tag>
</div> </div>
</div> </div>
<div v-if="showToggle" class="absolute right-2 top-2"> <div v-if="showToggle" class="absolute right-2 top-2">
......
...@@ -10,14 +10,28 @@ import DigitalAudioCard from './digital-audio-card.vue' ...@@ -10,14 +10,28 @@ import DigitalAudioCard from './digital-audio-card.vue'
const audioSettingStore = useAudioSettingStore() const audioSettingStore = useAudioSettingStore()
const digitalCreationStore = useDigitalCreationStore() const digitalCreationStore = useDigitalCreationStore()
const lanList = ref([ const lanList = ref([
{ key: LangType.CANTONESE, label: '粵語' }, { value: LangType.CANTONESE, label: '粵語' },
{ key: LangType.MANDARIN, label: '普通話' }, { value: LangType.MANDARIN, label: '普通話' },
]) ])
const sexValue = ref(0) const sexValue = ref(0)
const sexList = [ const sexList = [
{ key: 0, label: '女性' }, { key: 0, label: '女性' },
{ key: 1, label: '男性' }, { key: 1, label: '男性' },
] ]
const speedMarks: { [speed: number]: string } = {
3: '0.5x',
4: '0.8x',
5: '1x',
6: '1.3x',
7: '1.5x',
}
const pitchMarks: { [speed: number]: number } = {
3: 1,
4: 2,
5: 3,
6: 4,
7: 5,
}
const digitalTimbreValue = ref<TimbreItem>() const digitalTimbreValue = ref<TimbreItem>()
const digitalTimbreList = ref<TimbreItem[]>([]) const digitalTimbreList = ref<TimbreItem[]>([])
const digitalTimbreFemaleList = ref<TimbreItem[]>([]) const digitalTimbreFemaleList = ref<TimbreItem[]>([])
...@@ -43,14 +57,14 @@ const speed = computed({ ...@@ -43,14 +57,14 @@ const speed = computed({
}, },
}) })
const volume = computed({ // const volume = computed({
get() { // get() {
return Number(digitalCreationStore.volume) // return Number(digitalCreationStore.volume)
}, // },
set(value) { // set(value) {
digitalCreationStore.setVolume(String(value)) // digitalCreationStore.setVolume(String(value))
}, // },
}) // })
const pitch = computed({ const pitch = computed({
get() { get() {
...@@ -125,7 +139,13 @@ function handleClickAudioCard(timbreItem: TimbreItem) { ...@@ -125,7 +139,13 @@ function handleClickAudioCard(timbreItem: TimbreItem) {
<div class="h-full overflow-y-auto px-4 py-2"> <div class="h-full overflow-y-auto px-4 py-2">
<div v-if="!showAll"> <div v-if="!showAll">
<div class="flex justify-end pb-3"> <div class="flex justify-end pb-3">
<HorizontalTabs v-model:value="langType" :list="lanList" /> <n-select
v-model:value="langType"
class="w-[150px]!"
size="small"
:options="lanList"
placeholder="請選擇語言"
/>
</div> </div>
<DigitalAudioCard <DigitalAudioCard
...@@ -138,18 +158,18 @@ function handleClickAudioCard(timbreItem: TimbreItem) { ...@@ -138,18 +158,18 @@ function handleClickAudioCard(timbreItem: TimbreItem) {
<div class="mt-4 text-lg">聲音</div> <div class="mt-4 text-lg">聲音</div>
<div class="mt-4 flex items-center gap-2"> <div class="mt-4 flex items-center gap-2">
<div class="w-12">語速:</div> <div class="w-12">語速:</div>
<n-slider v-model:value="speed" class="flex-1" :max="15" :min="0" :step="1" /> <n-slider v-model:value="speed" class="flex-1" :max="7" :min="3" :step="1" :tooltip="false" />
<div class="w-10">{{ speed }}</div> <div class="w-10">{{ speedMarks[speed] }}</div>
</div> </div>
<div class="mt-4 flex items-center gap-2"> <!-- <div class="mt-4 flex items-center gap-2">
<div class="w-12">音量:</div> <div class="w-12">音量:</div>
<n-slider v-model:value="volume" class="flex-1" :max="15" :min="0" :step="1" /> <n-slider v-model:value="volume" class="flex-1" :max="7" :min="3" :step="1" :tooltip="false" />
<div class="w-10">{{ volume }}</div> <div class="w-10">{{ volume }}</div>
</div> </div> -->
<div v-if="langType === LangType.MANDARIN" class="mt-4 flex items-center gap-2"> <div v-if="langType === LangType.MANDARIN" class="mt-4 flex items-center gap-2">
<div class="w-12">語調:</div> <div class="w-12">語調:</div>
<n-slider v-model:value="pitch" class="flex-1" :max="15" :min="0" :step="1" /> <n-slider v-model:value="pitch" class="flex-1" :max="7" :min="3" :step="1" :tooltip="false" />
<div class="w-10">{{ pitch }}</div> <div class="w-10">{{ pitchMarks[pitch] }}</div>
</div> </div>
</div> </div>
......
...@@ -8,9 +8,11 @@ import { useRouter } from 'vue-router' ...@@ -8,9 +8,11 @@ import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
const digitalCreationStore = useDigitalCreationStore() const digitalCreationStore = useDigitalCreationStore()
const draftName = ref('')
const editDraftName = ref(false) const editDraftName = ref(false)
const autoSaveSuccess = ref(false) const autoSaveSuccess = ref(false)
const showExportModal = ref(false) const showExportModal = ref(false)
const isExporting = ref(false)
const ratioValue = ref(720) const ratioValue = ref(720)
const ratioList = [ const ratioList = [
{ value: 720, label: '720p' }, { value: 720, label: '720p' },
...@@ -37,7 +39,7 @@ watch( ...@@ -37,7 +39,7 @@ watch(
onMounted(() => { onMounted(() => {
timer = setInterval(() => { timer = setInterval(() => {
saveDraft() !isExporting.value && saveDraft()
}, 5000) }, 5000)
}) })
...@@ -46,12 +48,24 @@ onUnmounted(() => { ...@@ -46,12 +48,24 @@ onUnmounted(() => {
timer = null timer = null
}) })
function handleEditDraftName() {
draftName.value = digitalCreationStore.draftName
editDraftName.value = true
}
function handleUpdateDraftName() {
editDraftName.value = false
if (!draftName.value) {
window.$message.error('草稿名稱不能為空')
draftName.value = digitalCreationStore.draftName
return
}
digitalCreationStore.setDraftName(draftName.value)
}
// 保存为草稿 // 保存为草稿
async function saveDraft(autoSave: boolean = true) { async function saveDraft(autoSave: boolean = true) {
const payload: DraftConfig = { const payload: DraftConfig = { ...digitalCreationStore.$state }
...digitalCreationStore.$state,
draftName: digitalCreationStore.draftName,
}
const res = await saveDraftConfig<DraftConfig>(payload) const res = await saveDraftConfig<DraftConfig>(payload)
if (res.code === 0) { if (res.code === 0) {
autoSave ? (autoSaveSuccess.value = true) : window.$message.success('保存成功') autoSave ? (autoSaveSuccess.value = true) : window.$message.success('保存成功')
...@@ -69,17 +83,26 @@ function confirmExport() { ...@@ -69,17 +83,26 @@ function confirmExport() {
} }
async function createBaseVideoTask() { async function createBaseVideoTask() {
isExporting.value = true
const balance = await getUniversalCurrency() const balance = await getUniversalCurrency()
if (!balance) { if (!balance) {
isExporting.value = false
window.$message.error('餘額不足') window.$message.error('餘額不足')
return return
} }
if (!digitalCreationStore.id) { if (!digitalCreationStore.id) {
isExporting.value = false
window.$message.error('請先保存視頻為草稿') window.$message.error('請先保存視頻為草稿')
return return
} }
if (!digitalCreationStore.backgroundImageUrl) {
isExporting.value = false
window.$message.error('請選擇背景圖片')
return
}
if (!digitalCreationStore.inputAudioUrl) { if (!digitalCreationStore.inputAudioUrl) {
window.$message.error('請先生成預覽音頻') isExporting.value = false
window.$message.error('請生成預覽音頻')
return return
} }
const payload: BaseVideoTask = { const payload: BaseVideoTask = {
...@@ -109,17 +132,20 @@ async function getUniversalCurrency() { ...@@ -109,17 +132,20 @@ async function getUniversalCurrency() {
<template> <template>
<header class="flex h-14 items-center justify-between bg-white px-4"> <header class="flex h-14 items-center justify-between bg-white px-4">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<CustomIcon class="cursor-pointer text-lg" icon="mingcute:left-line" @click="router.replace('/')" /> <CustomIcon class="cursor-pointer text-lg" icon="mingcute:left-line" @click="router.back" />
<n-input <n-input
v-if="editDraftName" v-if="editDraftName"
v-model:value="digitalCreationStore.draftName" v-model:value="draftName"
placeholder="請輸入草稿名稱" placeholder="請輸入草稿名稱"
style="width: 400px" show-count
@blur="editDraftName = false" clearable
:maxlength="30"
class="w-96!"
@blur="handleUpdateDraftName"
/> />
<div v-else class="flex items-center gap-2"> <div v-else class="flex items-center gap-2">
<span>{{ digitalCreationStore.draftName }}</span> <span>{{ digitalCreationStore.draftName }}</span>
<CustomIcon class="cursor-pointer text-lg" icon="icon-park-outline:edit" @click="editDraftName = true" /> <CustomIcon class="text-theme-color cursor-pointer text-lg" icon="bxs:edit" @click="handleEditDraftName" />
</div> </div>
</div> </div>
...@@ -130,20 +156,14 @@ async function getUniversalCurrency() { ...@@ -130,20 +156,14 @@ async function getUniversalCurrency() {
<span>已自動保存</span> <span>已自動保存</span>
</div> </div>
<n-button class="!rounded-md" @click="saveDraft(false)"> 保存爲草稿 </n-button> <n-button class="!rounded-md" @click="saveDraft(false)"> 保存爲草稿 </n-button>
<n-button class="!rounded-md" type="info" @click="showExportModal = true"> 導出視頻 </n-button> <n-button class="!rounded-md" type="info" :disabled="!digitalCreationStore.id" @click="showExportModal = true">
導出視頻
</n-button>
</div> </div>
</div> </div>
</header> </header>
<n-modal <n-modal v-model:show="showExportModal" preset="dialog" title="導出視頻" @after-leave="isExporting = false">
v-model:show="showExportModal"
preset="dialog"
title="導出視頻"
positive-text="導出"
negative-text="取消"
@positive-click="confirmExport"
@negative-click="showExportModal = false"
>
<n-form ref="formRef" :label-width="120" label-placement="left"> <n-form ref="formRef" :label-width="120" label-placement="left">
<n-form-item label="視頻名稱" required> <n-form-item label="視頻名稱" required>
<n-input v-model:value="digitalCreationStore.videoName" placeholder="請輸入視頻名稱" /> <n-input v-model:value="digitalCreationStore.videoName" placeholder="請輸入視頻名稱" />
...@@ -170,5 +190,11 @@ async function getUniversalCurrency() { ...@@ -170,5 +190,11 @@ async function getUniversalCurrency() {
<n-radio value="mp4" checked> MP4 </n-radio> <n-radio value="mp4" checked> MP4 </n-radio>
</n-form-item> </n-form-item>
</n-form> </n-form>
<template #action>
<n-button @click="showExportModal = false">取消</n-button>
<n-button type="primary" :loading="isExporting" :disabled="isExporting" @click="confirmExport">
{{ isExporting ? '提交中…' : '導出' }}
</n-button>
</template>
</n-modal> </n-modal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { fetchDigitalHumanTemplateStatus, fetchDraftConfigById } from '@/apis/digital-creation' import { fetchDraftConfigById } from '@/apis/digital-creation'
import { fetchDigitalHumanTemplateStatus } from '@/apis/template'
import { useDigitalCreationStore } from '@/store/modules/creation' import { useDigitalCreationStore } from '@/store/modules/creation'
import { DigitalTemplate, DraftConfig, LangType } from '@/store/types/creation' import { DraftConfig, LangType } from '@/store/types/creation'
import { DigitalTemplate } from '@/store/types/template'
import { onMounted } from 'vue' import { onMounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import HeaderBar from './header-bar.vue' import HeaderBar from './header-bar.vue'
...@@ -26,8 +28,9 @@ async function getDigitalTemplate(id: number) { ...@@ -26,8 +28,9 @@ async function getDigitalTemplate(id: number) {
const draftConfig: DraftConfig = { const draftConfig: DraftConfig = {
id: null, id: null,
coverUrl: digitalTemplate.coverUrl, coverUrl: digitalTemplate.coverUrl,
draftName: `自定義草稿名稱 ${new Date().toLocaleString()}`, draftName: `新建數字人視頻`,
videoName: '', videoName: '',
videoDuration: null,
taskType: digitalTemplate.taskType, taskType: digitalTemplate.taskType,
requestId: digitalTemplate.requestId, requestId: digitalTemplate.requestId,
inputImageUrl: null, inputImageUrl: null,
...@@ -58,7 +61,7 @@ async function getDigitalTemplate(id: number) { ...@@ -58,7 +61,7 @@ async function getDigitalTemplate(id: number) {
logoUrl: digitalTemplate.logoParams?.logoUrl || null, logoUrl: digitalTemplate.logoParams?.logoUrl || null,
bgmUrl: digitalTemplate.bgmParams?.bgmUrl || null, bgmUrl: digitalTemplate.bgmParams?.bgmUrl || null,
materialUrl: digitalTemplate.materialUrl, materialUrl: digitalTemplate.materialUrl,
pronunciationLanguageL: LangType.CANTONESE, pronunciationLanguage: LangType.CANTONESE,
} }
digitalCreationStore.updateDigitalCreation(draftConfig) digitalCreationStore.updateDigitalCreation(draftConfig)
} }
......
<script setup lang="ts"> <script setup lang="ts">
// import { fetchRecentCreationList } from '@/apis/drafts' import { fetchDraftsList } from '@/apis/drafts'
// import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import { DraftConfig } from '@/store/types/creation'
import { TaskType } from '@/store/types/template'
import { Right } from '@icon-park/vue-next' import { Right } from '@icon-park/vue-next'
import { onMounted } from 'vue' import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
// const userStore = useUserStore() const userStore = useUserStore()
// const token = userStore.token const token = userStore.token
const recentCreationList = ref<DraftConfig[]>([])
onMounted(() => { onMounted(() => {
// getRecentCreationList() getDraftsList()
}) })
async function getDraftsList() {
const res = await fetchDraftsList<DraftConfig[]>(token, { pagingInfo: { pageNo: 1, pageSize: 7 } })
if (res.code === 0) {
recentCreationList.value = res.data
}
}
function formatTaskType(taskType: TaskType) {
switch (taskType) {
case TaskType.IMAGE_VIDEO: {
return '照片數字人'
}
case TaskType.BASE_VIDEO: {
return '基礎數字人'
}
case TaskType.ADVANCED_VIDEO: {
return '高級數字人'
}
}
}
function handleGoToDrafts() { function handleGoToDrafts() {
router.push('/work/draft') router.push('/work/draft')
} }
// function getRecentCreationList() {
// fetchRecentCreationList(token).then((res) => { function handleClickDraft(item: DraftConfig) {
// if (res.code !== 0) return '' router.push(`/creation/${item.templateId}/${item.id}`)
// console.log(res) }
// })
// }
</script> </script>
<template> <template>
...@@ -34,29 +57,27 @@ function handleGoToDrafts() { ...@@ -34,29 +57,27 @@ function handleGoToDrafts() {
</div> </div>
</div> </div>
<div class="flex flex-wrap justify-between"> <div class="flex flex-wrap justify-between">
<div v-for="item in 7" :key="item"> <div v-for="(item, index) in recentCreationList" :key="index">
<n-card class="mt-[16px] rounded-[10px]"> <div class="mt-[16px] overflow-hidden rounded-[10px]" @click="handleClickDraft(item)">
<template #cover> <div class="relative flex h-[145px] w-[145px] items-center justify-center bg-[#f0f0f0]">
<div class="relative flex h-[145px] w-[145px] items-center justify-center bg-[#f0f0f0]"> <img
<img :src="item.coverUrl!"
src="https://meta-human-editor-prd.cdn.bcebos.com/2024-04-17T16:53:29.223639/sd-qdmv5bsghiyx1xb9z_1713344008761.png?x-bce-process=image/format,f_auto/resize,w_500/quality,q_90" class="absolute h-full w-full scale-100 cursor-pointer rounded-[12px] object-cover blur-[32px] filter transition-transform duration-300 ease-in-out"
class="absolute h-full w-full scale-100 cursor-pointer rounded-[12px] object-cover blur-[32px] filter transition-transform duration-300 ease-in-out" />
/> <img
<img :src="item.coverUrl!"
src="https://meta-human-editor-prd.cdn.bcebos.com/2024-04-17T16:53:29.223639/sd-qdmv5bsghiyx1xb9z_1713344008761.png?x-bce-process=image/format,f_auto/resize,w_500/quality,q_90" class="hover:scale-104 absolute inset-0 aspect-[1] cursor-pointer object-contain transition-transform duration-300 ease-in-out"
class="hover:scale-104 absolute inset-0 aspect-[1] cursor-pointer object-contain transition-transform duration-300 ease-in-out" />
/>
<div <div
class="absolute bottom-[8px] left-[8px] cursor-default rounded-[4px] bg-[#000000]/[.5] px-[6px] py-[2px] text-[12px] text-[#FFFFFF]" class="absolute bottom-[8px] left-[8px] cursor-default rounded-[4px] bg-[#000000]/[.5] px-[6px] py-[2px] text-[12px] text-[#FFFFFF]"
> >
精编视频 {{ formatTaskType(item.taskType) }}
</div>
</div> </div>
</template> </div>
</n-card> </div>
<n-ellipsis class="mt-[12px] cursor-default text-[#151b26]" style="max-width: 150px"> <n-ellipsis class="mt-[12px] cursor-default text-[#151b26]" style="max-width: 150px">
金融课程2024-09-11 09:48:41 {{ item.draftName }}
</n-ellipsis> </n-ellipsis>
</div> </div>
</div> </div>
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { fetchDigitalHumanTemplateStatusList } from '@/apis/template.ts'
import TemplatePreviewModal from './template-preview-modal.vue' import { DigitalTemplate } from '@/store/types/template.ts'
import { useInfiniteScroll } from '@vueuse/core' import { useInfiniteScroll } from '@vueuse/core'
import { templateData } from '../templateData.ts' import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import TemplatePreviewModal from './template-preview-modal.vue'
const router = useRouter()
const PreviewModalVisible = ref(false) const PreviewModalVisible = ref(false)
const selectedTemplate = ref({ const selectedTemplate = ref<DigitalTemplate>()
imageUrl: '', const checkedClassifyValue = ref('')
videoUrl: '',
})
const checkedClassifyValue = ref('Trending')
const loadingMoreTemplate = ref(false) const loadingMoreTemplate = ref(false)
const templatePageSize = ref(30) const templatePageSize = ref(30)
const isHoverTemplate = ref<{ [key: string]: boolean }>({}) const isHoverTemplate = ref<{ [key: string]: boolean }>({})
const templateBottomEl = ref<HTMLElement | null>(null) const templateBottomEl = ref<HTMLElement | null>(null)
const templateClassify = [ const templateClassify = [
{ value: 'Trending', label: '熱門' }, { value: '', label: '熱門' },
{ { value: 'FINANCIAL_MARKETING', label: '理財營銷' },
value: 'Shakermaker', { value: 'EDUCATION_LEARNING', label: '教育學習' },
label: '産品營銷', { value: 'FESTIVAL_HOTS_SPOTS', label: '節日熱點' },
},
{
value: 'NewsInformatio',
label: '新聞資訊',
},
{
value: 'BusinessCard',
label: '名片',
},
{
value: 'Health',
label: '醫療健康',
},
{
value: 'Education',
label: '教育培訓',
},
{
value: 'Invite',
label: '邀請函',
},
] ]
const templateList = ref(templateData) const templateList = ref<DigitalTemplate[]>([])
const filteredTemplateData = computed(() => { const filteredTemplateData = computed(() => {
if (!checkedClassifyValue.value) { if (!checkedClassifyValue.value) {
return templateList.value return templateList.value
} }
return templateList.value.filter((item: { templateType: string }) => item.templateType === checkedClassifyValue.value) return templateList.value.filter((item) => item.templateType === checkedClassifyValue.value)
}) })
const displayedData = computed(() => { const displayedData = computed(() => {
...@@ -58,6 +38,7 @@ const displayedData = computed(() => { ...@@ -58,6 +38,7 @@ const displayedData = computed(() => {
const canLoadMore = computed(() => { const canLoadMore = computed(() => {
return templatePageSize.value < filteredTemplateData.value.length return templatePageSize.value < filteredTemplateData.value.length
}) })
useInfiniteScroll( useInfiniteScroll(
templateBottomEl, templateBottomEl,
() => { () => {
...@@ -73,18 +54,31 @@ useInfiniteScroll( ...@@ -73,18 +54,31 @@ useInfiniteScroll(
{ distance: 10 }, { distance: 10 },
) )
function handleOpenModal(template: typeof selectedTemplate.value) { onMounted(() => {
getTemplateList()
})
async function getTemplateList() {
const res = await fetchDigitalHumanTemplateStatusList<DigitalTemplate[]>()
if (res.code === 0) {
templateList.value = res.data
}
}
function handleOpenModal(template: DigitalTemplate) {
selectedTemplate.value = { ...template } selectedTemplate.value = { ...template }
PreviewModalVisible.value = true PreviewModalVisible.value = true
} }
function handleToCreation(template: DigitalTemplate) {
router.push(`/creation/${template.id}`)
}
</script> </script>
<template> <template>
<div <div class="mt-[16px] min-h-full min-w-[1160px] overscroll-none rounded-[16px] bg-white px-[24px] pb-[16px]">
class="mt-[16px] h-full min-h-[980px] min-w-[1160px] overscroll-none rounded-[16px] bg-white px-[24px] pb-[16px]"
>
<div class="sticky top-0 z-10 bg-white pt-[24px]"> <div class="sticky top-0 z-10 bg-white pt-[24px]">
<div class="mb-[24px] cursor-default text-[18px] text-[#000]">推薦模</div> <div class="mb-[24px] cursor-default text-[18px] text-[#000]">推薦模</div>
<div class="pb-[16px]"> <div class="pb-[16px]">
<n-radio-group v-model:value="checkedClassifyValue"> <n-radio-group v-model:value="checkedClassifyValue">
<n-radio-button <n-radio-button
...@@ -110,18 +104,18 @@ function handleOpenModal(template: typeof selectedTemplate.value) { ...@@ -110,18 +104,18 @@ function handleOpenModal(template: typeof selectedTemplate.value) {
<div class="relative flex items-center justify-center bg-[#f0f0f0]"> <div class="relative flex items-center justify-center bg-[#f0f0f0]">
<img <img
v-if="!isHoverTemplate[item.id]" v-if="!isHoverTemplate[item.id]"
:src="item.imageUrl" :src="item.coverUrl"
class="hover:scale-104 inset-0 w-full cursor-pointer object-cover transition-transform duration-300 ease-in-out" class="hover:scale-104 inset-0 w-full cursor-pointer object-cover transition-transform duration-300 ease-in-out"
@click="handleOpenModal(item)" @click="handleOpenModal(item)"
/> />
<video <video
v-else v-else
:src="item.videoUrl" :src="item.demonstrationVideoUrl"
class="hover:scale-104 inset-0 w-full cursor-pointer object-cover transition-transform duration-300 ease-in-out" class="hover:scale-104 inset-0 w-full cursor-pointer object-cover transition-transform duration-300 ease-in-out"
autoplay autoplay
muted muted
:poster="item.imageUrl" :poster="item.coverUrl"
@click="handleOpenModal(item)" @click="handleOpenModal(item)"
></video> ></video>
<div <div
...@@ -129,6 +123,7 @@ function handleOpenModal(template: typeof selectedTemplate.value) { ...@@ -129,6 +123,7 @@ function handleOpenModal(template: typeof selectedTemplate.value) {
></div> ></div>
<div <div
class="overlay absolute bottom-2 left-2 right-2 hidden h-8 cursor-pointer rounded-[6px] border border-gray-300 bg-white/90 px-2 text-center text-[14px] leading-[30px] text-[#000000]" class="overlay absolute bottom-2 left-2 right-2 hidden h-8 cursor-pointer rounded-[6px] border border-gray-300 bg-white/90 px-2 text-center text-[14px] leading-[30px] text-[#000000]"
@click="handleToCreation(item)"
> >
做同款 做同款
</div> </div>
...@@ -143,7 +138,7 @@ function handleOpenModal(template: typeof selectedTemplate.value) { ...@@ -143,7 +138,7 @@ function handleOpenModal(template: typeof selectedTemplate.value) {
<div class="relative top-[10px] h-[1px] w-[14px] bg-[#a9b4cc]"></div> <div class="relative top-[10px] h-[1px] w-[14px] bg-[#a9b4cc]"></div>
</div> </div>
<TemplatePreviewModal v-model="PreviewModalVisible" :selected-template="selectedTemplate" /> <TemplatePreviewModal v-model="PreviewModalVisible" :selected-template="selectedTemplate!" />
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
......
<script setup lang="ts"> <script setup lang="ts">
import { DigitalTemplate } from '@/store/types/template'
import { Close } from '@icon-park/vue-next' import { Close } from '@icon-park/vue-next'
const PreviewModalVisible = defineModel<boolean>() const PreviewModalVisible = defineModel<boolean>()
const props = defineProps<{ const props = defineProps<{
selectedTemplate: { selectedTemplate: DigitalTemplate
id: number
coverUrl: string
demonstrationGifUrl: string
demonstrationVideoUrl: string
templateName: string
templateType: string
}
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'go-to-create', id: number): void (event: 'go-to-create', id: number): void
......
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