Commit bfc4695a authored by Dazzle Wu's avatar Dazzle Wu

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

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