feat: course generation
This commit is contained in:
7
app.vue
7
app.vue
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ModalAuthentication from '~/components/ModalAuthentication.vue';
|
import ModalAuthentication from '~/components/ModalAuthentication.vue'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -8,7 +8,7 @@ const loginState = useLoginState()
|
|||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
titleTemplate(title) {
|
titleTemplate(title) {
|
||||||
return title ? `${title} - 眩生花 AI 助手` : '眩生花 AI 助手'
|
return title ? `${ title } - 眩生花 AI 助手` : '眩生花 AI 助手'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -33,13 +33,14 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<NuxtLoadingIndicator />
|
<NuxtLoadingIndicator/>
|
||||||
|
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage/>
|
<NuxtPage/>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
|
|
||||||
<UModals/>
|
<UModals/>
|
||||||
|
<USlideovers/>
|
||||||
<UNotifications/>
|
<UNotifications/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -16,15 +16,21 @@ const props = defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative font-sans select-none">
|
<div class="relative font-sans select-none flex justify-between items-center">
|
||||||
<h1
|
<div>
|
||||||
v-if="subtitle"
|
<h1
|
||||||
class="text-base text-neutral-300 italic tracking-wide font-black leading-none"
|
v-if="subtitle"
|
||||||
>{{ subtitle }}</h1>
|
class="text-base text-neutral-300 italic tracking-wide font-black leading-none"
|
||||||
|
>{{ subtitle }}</h1>
|
||||||
|
|
||||||
<h1 class="text-xl font-bold text-neutral-700 leading-none relative z-[1]">
|
<h1 class="text-xl font-bold text-neutral-700 leading-none relative z-[1]">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h1>
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<slot name="action"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="bubble"
|
v-if="bubble"
|
||||||
|
|||||||
@@ -14,27 +14,27 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits({
|
const emit = defineEmits({
|
||||||
close: () => true,
|
close: () => true,
|
||||||
select: (value: DigitalHumanItem) => value,
|
select: (digitalHumans: DigitalHumanItem | DigitalHumanItem[]) => digitalHumans,
|
||||||
})
|
})
|
||||||
|
|
||||||
const loginState = useLoginState()
|
const loginState = useLoginState()
|
||||||
const modal = useModal()
|
const modal = useModal()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
|
||||||
const selectedDigitalHumans = ref<DigitalHumanItem[]>([])
|
const selectedDigitalHumans = ref<DigitalHumanItem[]>([])
|
||||||
const handleSelectClick = (item: DigitalHumanItem) => {
|
const handleSelectClick = (item: DigitalHumanItem) => {
|
||||||
if (props.multiple) {
|
// 如果点击的项目已经在已选列表中,则移除;否则添加
|
||||||
if (selectedDigitalHumans.value.includes(item)) {
|
if (selectedDigitalHumans.value.includes(item)) {
|
||||||
selectedDigitalHumans.value = selectedDigitalHumans.value.filter(d => d !== item)
|
selectedDigitalHumans.value = selectedDigitalHumans.value.filter(d => d !== item)
|
||||||
} else {
|
|
||||||
selectedDigitalHumans.value.push(item)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
selectedDigitalHumans.value = [item]
|
selectedDigitalHumans.value = props.multiple ? [...selectedDigitalHumans.value, item] : [item]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
selectedDigitalHumans.value = []
|
||||||
if (props.isOpen) {
|
if (props.isOpen) {
|
||||||
emit('close')
|
emit('close')
|
||||||
} else {
|
} else {
|
||||||
@@ -42,15 +42,33 @@ const handleClose = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (selectedDigitalHumans.value.length === 0) {
|
||||||
|
toast.add({
|
||||||
|
title: '请选择数字人',
|
||||||
|
description: '请至少选择一个数字人',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-circle-x',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('select', props.multiple ? selectedDigitalHumans.value : selectedDigitalHumans.value[0])
|
||||||
|
handleClose()
|
||||||
|
setTimeout(() => {
|
||||||
|
page.value = 1
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
const tabItems = [{
|
const tabItems = [{
|
||||||
key: 'user',
|
key: 'user',
|
||||||
label: '用户数字人',
|
label: '我的数字人',
|
||||||
icon: 'i-tabler-user',
|
icon: 'i-tabler-user',
|
||||||
}, {
|
|
||||||
key: 'system',
|
|
||||||
label: '系统数字人',
|
|
||||||
icon: 'i-tabler-user-star',
|
|
||||||
}]
|
}]
|
||||||
|
const tabIndex = ref(0)
|
||||||
|
|
||||||
|
watch(tabIndex, () => {
|
||||||
|
page.value = 1
|
||||||
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: userDigitalList,
|
data: userDigitalList,
|
||||||
@@ -62,23 +80,12 @@ const {
|
|||||||
token: loginState.token!,
|
token: loginState.token!,
|
||||||
user_id: loginState.user.id,
|
user_id: loginState.user.id,
|
||||||
to_user_id: loginState.user.id,
|
to_user_id: loginState.user.id,
|
||||||
page: 1,
|
page: page.value,
|
||||||
perpage: 20,
|
perpage: 15,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
transform: res => {
|
watch: [page],
|
||||||
if (res.ret !== 200) {
|
|
||||||
toast.add({
|
|
||||||
title: '获取数字人列表失败',
|
|
||||||
description: res.msg || '未知错误',
|
|
||||||
color: 'red',
|
|
||||||
icon: 'i-tabler-circle-x',
|
|
||||||
})
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return res?.data.items || []
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -92,23 +99,12 @@ const {
|
|||||||
token: loginState.token!,
|
token: loginState.token!,
|
||||||
user_id: loginState.user.id,
|
user_id: loginState.user.id,
|
||||||
to_user_id: loginState.user.id,
|
to_user_id: loginState.user.id,
|
||||||
page: 1,
|
page: page.value,
|
||||||
perpage: 20,
|
perpage: 15,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
transform: res => {
|
watch: [page],
|
||||||
if (res.ret !== 200) {
|
|
||||||
toast.add({
|
|
||||||
title: '获取系统数字人列表失败',
|
|
||||||
description: res.msg || '未知错误',
|
|
||||||
color: 'red',
|
|
||||||
icon: 'i-tabler-circle-x',
|
|
||||||
})
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return res?.data.items || []
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
@@ -116,7 +112,7 @@ const {
|
|||||||
<template>
|
<template>
|
||||||
<UModal
|
<UModal
|
||||||
:model-value="isOpen"
|
:model-value="isOpen"
|
||||||
:ui="{ width: 'w-full sm:max-w-4xl' }"
|
:ui="{ width: 'w-full sm:max-w-3xl' }"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
||||||
@@ -136,30 +132,49 @@ const {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<UTabs
|
<UTabs
|
||||||
:items="tabItems"
|
:items="[
|
||||||
|
...tabItems,
|
||||||
|
loginState.user.auth_code === 2 ? {
|
||||||
|
key: 'system',
|
||||||
|
label: '系统数字人',
|
||||||
|
icon: 'i-tabler-user-star',
|
||||||
|
} : null
|
||||||
|
].filter(Boolean)"
|
||||||
|
@change="i => tabIndex = i"
|
||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
<div v-if="item.key === 'user'" class="w-full grid grid-cols-2 sm:grid-cols-4 gap-4">
|
<div class="w-full grid grid-cols-3 sm:grid-cols-5 gap-4">
|
||||||
<div
|
<div
|
||||||
v-for="(d, i) in userDigitalList"
|
v-for="(d, i) in item.key === 'user' ? userDigitalList?.data.items : systemDigitalList?.data.items"
|
||||||
:key="`user-digital-${d.model_id}`" class="relative overflow-hidden w-full aspect-[10/16] bg-white rounded-md shadow-md transition-all ring-2 ring-primary-400"
|
:key="`${item.key === 'user' ? 'user' : 'system'}-digital-${d.model_id}`"
|
||||||
|
:class="{'border-primary shadow-md': selectedDigitalHumans.includes(d)}"
|
||||||
|
class="relative flex flex-col justify-center items-center gap-2 overflow-hidden w-full bg-white rounded-md border cursor-pointer transition-all duration-150 select-none"
|
||||||
@click="handleSelectClick(d)"
|
@click="handleSelectClick(d)"
|
||||||
>
|
>
|
||||||
<div class="absolute w-full h-full -top-[35%] -left-[15%] rotate-[60deg] bg-primary-400"></div>
|
<div
|
||||||
<div class="absolute left-0 bottom-[44%] rotate-[-30deg]">
|
:class="{'bg-primary-50': selectedDigitalHumans.includes(d)}"
|
||||||
<span class="text-xs text-white font-semibold">{{ d.description }}</span>
|
class="relative bg-neutral-100 border-b w-full aspect-square object-cover overflow-hidden transition-all duration-150"
|
||||||
|
>
|
||||||
|
<NuxtImg :src="d.avatar" class="-translate-y-4"/>
|
||||||
|
<UIcon v-if="selectedDigitalHumans.includes(d)" class="absolute top-1 right-1 text-lg text-primary"
|
||||||
|
name="i-tabler-check"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-1 bottom-[34%] rotate-[-30deg]">
|
<div class="w-full flex flex-col gap-1 px-2 pb-2">
|
||||||
<span class="text-lg text-primary font-semibold">{{ d.name }}</span>
|
<div class="flex justify-between items-center">
|
||||||
|
<span class="text-sm text-neutral-800 font-medium line-clamp-1">{{ d.name }}</span>
|
||||||
|
<span class="text-xs text-neutral-300 font-medium">ID:{{ d.digital_human_id || d.id }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 bottom-0 rounded-tr bg-primary-400 px-1 pb-1">
|
|
||||||
<span class="text-xs text-white leading-none font-black">·SELECTED·</span>
|
|
||||||
</div>
|
|
||||||
<NuxtImg :src="d.avatar" class="absolute bottom-0 -right-7 h-full"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.key === 'system'">
|
<div class="flex justify-end">
|
||||||
system
|
<UPagination
|
||||||
|
v-if="(item.key === 'user' ? (userDigitalList?.data.total || 0) : (systemDigitalList?.data.total || 0)) > 0"
|
||||||
|
v-model="page"
|
||||||
|
:page-count="15"
|
||||||
|
:total="item.key === 'user' ? (userDigitalList?.data.total || 0) : (systemDigitalList?.data.total || 0)"
|
||||||
|
class="pt-4"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UTabs>
|
</UTabs>
|
||||||
@@ -167,7 +182,7 @@ const {
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-medium opacity-50">
|
<p class="text-xs font-medium opacity-50 select-none">
|
||||||
如果没有出现您的数字人,请联系管理员开通
|
如果没有出现您的数字人,请联系管理员开通
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,8 +196,8 @@ const {
|
|||||||
<UButton
|
<UButton
|
||||||
color="primary"
|
color="primary"
|
||||||
label="选择"
|
label="选择"
|
||||||
type="submit"
|
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
@click="handleSubmit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
240
components/SlideCreateCourse.vue
Normal file
240
components/SlideCreateCourse.vue
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import FileDnD from '~/components/uni/FileDnD/index.vue'
|
||||||
|
import { type InferType, number, object, string } from 'yup'
|
||||||
|
import ModalDigitalHumanSelect from '~/components/ModalDigitalHumanSelect.vue'
|
||||||
|
import type { FormSubmitEvent } from '#ui/types'
|
||||||
|
import { useFetchWrapped } from '~/composables/useFetchWrapped'
|
||||||
|
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
|
||||||
|
const slide = useSlideover()
|
||||||
|
const toast = useToast()
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
|
const creationForm = ref<HTMLFormElement>()
|
||||||
|
const creationPending = ref(false)
|
||||||
|
const isDigitalSelectorOpen = ref(false)
|
||||||
|
|
||||||
|
const createCourseSchema = object({
|
||||||
|
task_title: string().trim().min(4, '标题必须大于4个字符').max(20, '标题不能超过20个字符').required('请输入微课标题'),
|
||||||
|
digital_human_id: number().not([0], '请选择数字人'),
|
||||||
|
gen_server: string().required(),
|
||||||
|
speed: number().default(1.0).min(0.5).max(1.5).required(),
|
||||||
|
})
|
||||||
|
|
||||||
|
type CreateCourseSchema = InferType<typeof createCourseSchema>
|
||||||
|
|
||||||
|
const createCourseState = reactive({
|
||||||
|
task_title: undefined,
|
||||||
|
digital_human_id: 0,
|
||||||
|
gen_server: 'main',
|
||||||
|
speed: 1.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const selected_file = ref<File[] | null>(null)
|
||||||
|
const selected_digital_human = ref<DigitalHumanItem | null>(null)
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (selected_digital_human.value) {
|
||||||
|
createCourseState.digital_human_id = selected_digital_human.value.model_id || selected_digital_human.value.id!
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onCreateCourseSubmit = async (event: FormSubmitEvent<CreateCourseSchema>) => {
|
||||||
|
console.log(event.data)
|
||||||
|
if (!selected_file.value) {
|
||||||
|
toast.add({
|
||||||
|
title: '未选择文件',
|
||||||
|
description: '请先选择 PPTX 文件',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
creationPending.value = true
|
||||||
|
// upload PPTX file
|
||||||
|
useFileGo(selected_file.value[0]).then(url => {
|
||||||
|
useFetchWrapped<req.gen.CourseGenCreate & AuthedRequest, BaseResponse<resp.gen.CourseGenCreate>>('App.Digital_Convert.Create', {
|
||||||
|
token: loginState.token!,
|
||||||
|
user_id: loginState.user.id,
|
||||||
|
task_title: event.data.task_title,
|
||||||
|
gen_server: event.data.gen_server as 'main' | 'standby1',
|
||||||
|
speed: 2 - event.data.speed,
|
||||||
|
ppt_url: url,
|
||||||
|
digital_human_id: event.data.digital_human_id,
|
||||||
|
custom_video: '[]',
|
||||||
|
opening_url: '',
|
||||||
|
ending_url: '',
|
||||||
|
}).then(res => {
|
||||||
|
if (res.data.record_status === 1) {
|
||||||
|
toast.add({
|
||||||
|
title: '创建成功',
|
||||||
|
description: '已加入生成队列',
|
||||||
|
color: 'green',
|
||||||
|
icon: 'i-tabler-check',
|
||||||
|
})
|
||||||
|
emit('success')
|
||||||
|
slide.close()
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
title: '创建失败',
|
||||||
|
description: res.msg || '未知错误',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
creationPending.value = false
|
||||||
|
}).catch(e => {
|
||||||
|
creationPending.value = false
|
||||||
|
toast.add({
|
||||||
|
title: '创建失败',
|
||||||
|
description: e.message || '未知错误',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USlideover prevent-close>
|
||||||
|
<UCard
|
||||||
|
:ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
|
||||||
|
class="flex flex-col flex-1"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
新建微课视频
|
||||||
|
</h3>
|
||||||
|
<UButton
|
||||||
|
class="-my-1"
|
||||||
|
color="gray"
|
||||||
|
icon="i-tabler-x"
|
||||||
|
variant="ghost"
|
||||||
|
@click="slide.close()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UForm
|
||||||
|
ref="creationForm"
|
||||||
|
:schema="createCourseSchema"
|
||||||
|
:state="createCourseState"
|
||||||
|
class="space-y-4"
|
||||||
|
@submit="onCreateCourseSubmit"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between gap-2 *:flex-1">
|
||||||
|
<UFormGroup label="微课标题" name="task_title" required>
|
||||||
|
<UInput v-model="createCourseState.task_title" placeholder="请输入微课标题"/>
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<UFormGroup label="数字人" name="digital_human_id" required>
|
||||||
|
<div
|
||||||
|
:class="{'shadow-inner': !!selected_digital_human}"
|
||||||
|
class="flex items-center gap-2 bg-neutral-100 p-2 rounded-md cursor-pointer select-none transition-all"
|
||||||
|
@click="isDigitalSelectorOpen = true"
|
||||||
|
>
|
||||||
|
<div class="w-12 aspect-square border rounded-md flex justify-center items-center overflow-hidden">
|
||||||
|
<UIcon v-if="!selected_digital_human" class="text-2xl opacity-50" name="i-tabler-user-screen"/>
|
||||||
|
<NuxtImg v-else :src="selected_digital_human?.avatar"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col text-neutral-400 text-sm font-medium">
|
||||||
|
<span :class="!!selected_digital_human ? 'text-neutral-600' : ''">{{
|
||||||
|
selected_digital_human?.name || '点击选择数字人'
|
||||||
|
}}</span>
|
||||||
|
<span v-if="selected_digital_human?.description" class="text-2xs">
|
||||||
|
{{ selected_digital_human?.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="视频片头片尾" name="opening">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 bg-neutral-100 p-2 rounded-md cursor-pointer select-none transition-all"
|
||||||
|
>
|
||||||
|
<div class="w-12 aspect-square border rounded-md flex justify-center items-center">
|
||||||
|
<UIcon class="text-2xl opacity-50" name="i-tabler-brackets-contain"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col text-neutral-400 text-sm font-medium">
|
||||||
|
<span>点击选择</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UFormGroup label="PPT 文件" required>
|
||||||
|
<FileDnD
|
||||||
|
accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||||
|
@change="file => selected_file = file"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UAccordion :items="[{label: '高级选项'}]" color="gray" size="lg">
|
||||||
|
<template #item>
|
||||||
|
<div class="border rounded-lg space-y-4 p-4 pb-6">
|
||||||
|
<UFormGroup label="生成线路" name="gen_server">
|
||||||
|
<USelectMenu
|
||||||
|
v-model="createCourseState.gen_server"
|
||||||
|
:options="[{
|
||||||
|
label: '主线路',
|
||||||
|
value: 'main',
|
||||||
|
}, {
|
||||||
|
label: '备用线路',
|
||||||
|
value: 'standby1',
|
||||||
|
}]"
|
||||||
|
option-attribute="label"
|
||||||
|
value-attribute="value"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup :label="`视频倍速:${createCourseState.speed}`" name="speed">
|
||||||
|
<URange
|
||||||
|
v-model="createCourseState.speed"
|
||||||
|
:max="1.5"
|
||||||
|
:min="0.5"
|
||||||
|
:step="0.1"
|
||||||
|
class="pt-4"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UAccordion>
|
||||||
|
</UForm>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end space-x-4">
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
label="取消"
|
||||||
|
size="lg"
|
||||||
|
variant="ghost"
|
||||||
|
@click="slide.close()"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
:loading="creationPending"
|
||||||
|
color="primary"
|
||||||
|
label="提交"
|
||||||
|
size="lg"
|
||||||
|
variant="solid"
|
||||||
|
@click="creationForm?.submit()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
<ModalDigitalHumanSelect
|
||||||
|
:is-open="isDigitalSelectorOpen"
|
||||||
|
@close="isDigitalSelectorOpen = false"
|
||||||
|
@select="digitalHumans => {
|
||||||
|
selected_digital_human = (digitalHumans as DigitalHumanItem)
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</USlideover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -3,7 +3,6 @@ import type { PropType } from 'vue'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useDownload } from '~/composables/useDownload'
|
import { useDownload } from '~/composables/useDownload'
|
||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
import SRTEditor from '~/components/aigc/course-generate/SRTEditor.vue'
|
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { metaSymbol } = useShortcuts()
|
const { metaSymbol } = useShortcuts()
|
||||||
@@ -285,7 +284,7 @@ const copyTaskId = (extraMessage?: string) => {
|
|||||||
/>
|
/>
|
||||||
</UCard>
|
</UCard>
|
||||||
</UModal>
|
</UModal>
|
||||||
<SRTEditor
|
<LazyAigcCourseGenerateSRTEditor
|
||||||
ref="srtEditor"
|
ref="srtEditor"
|
||||||
:course="course"
|
:course="course"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,308 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import CGTaskCard from '~/components/aigc/course-generate/CGTaskCard.vue'
|
|
||||||
import FileDnD from '~/components/uni/FileDnD/index.vue'
|
|
||||||
import { useFetchWrapped } from '~/composables/useFetchWrapped'
|
|
||||||
import { type InferType, number, object, string } from 'yup'
|
|
||||||
import type { FormSubmitEvent } from '#ui/types'
|
|
||||||
import ModalAuthentication from '~/components/ModalAuthentication.vue'
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const modal = useModal()
|
|
||||||
const loginState = useLoginState()
|
|
||||||
|
|
||||||
const isCreateCourseModalOpen = ref(false)
|
|
||||||
const creationPending = ref(false)
|
|
||||||
const deletePending = ref(false)
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: courseList,
|
|
||||||
pending: courseListPending,
|
|
||||||
refresh: refreshCourseList,
|
|
||||||
} = useAsyncData(
|
|
||||||
() => useFetchWrapped<
|
|
||||||
req.gen.CourseGenList & AuthedRequest,
|
|
||||||
BaseResponse<PagedData<resp.gen.CourseGenItem>>
|
|
||||||
>('App.Digital_Convert.GetList', {
|
|
||||||
token: loginState.token!,
|
|
||||||
user_id: loginState.user.id,
|
|
||||||
to_user_id: loginState.user.id,
|
|
||||||
page: 1,
|
|
||||||
perpage: 10,
|
|
||||||
}), {
|
|
||||||
transform: res => res?.data.items || [],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const onCreateCourseClick = () => {
|
|
||||||
isCreateCourseModalOpen.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCourseSchema = object({
|
|
||||||
task_title: string().trim().min(4, '标题必须大于4个字符').max(20, '标题不能超过20个字符').required('请输入微课标题'),
|
|
||||||
gen_server: string().required(),
|
|
||||||
speed: number().default(1.0).min(0.5).max(1.5).required(),
|
|
||||||
})
|
|
||||||
|
|
||||||
type CreateCourseSchema = InferType<typeof createCourseSchema>
|
|
||||||
|
|
||||||
const createCourseState = reactive({
|
|
||||||
task_title: undefined,
|
|
||||||
gen_server: 'main',
|
|
||||||
speed: 1.0,
|
|
||||||
})
|
|
||||||
|
|
||||||
const selected_file = ref<File[] | null>(null)
|
|
||||||
|
|
||||||
const onCreateCourseSubmit = async (event: FormSubmitEvent<CreateCourseSchema>) => {
|
|
||||||
console.log(event.data)
|
|
||||||
if (!selected_file.value) {
|
|
||||||
toast.add({
|
|
||||||
title: '未选择文件',
|
|
||||||
description: '请先选择 PPTX 文件',
|
|
||||||
color: 'red',
|
|
||||||
icon: 'i-tabler-alert-triangle',
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
creationPending.value = true
|
|
||||||
// upload PPTX file
|
|
||||||
useFileGo(selected_file.value[0]).then(url => {
|
|
||||||
useFetchWrapped<req.gen.CourseGenCreate & AuthedRequest, BaseResponse<resp.gen.CourseGenCreate>>('App.Digital_Convert.Create', {
|
|
||||||
token: loginState.token!,
|
|
||||||
user_id: loginState.user.id,
|
|
||||||
task_title: event.data.task_title,
|
|
||||||
gen_server: event.data.gen_server as 'main' | 'standby1',
|
|
||||||
speed: 2 - event.data.speed,
|
|
||||||
ppt_url: url,
|
|
||||||
digital_human_id: 40696,
|
|
||||||
custom_video: '[]',
|
|
||||||
opening_url: '',
|
|
||||||
ending_url: '',
|
|
||||||
}).then(res => {
|
|
||||||
if (res.data.record_status === 1) {
|
|
||||||
toast.add({
|
|
||||||
title: '创建成功',
|
|
||||||
description: '微课视频已开始生成',
|
|
||||||
color: 'green',
|
|
||||||
icon: 'i-tabler-check',
|
|
||||||
})
|
|
||||||
refreshCourseList()
|
|
||||||
isCreateCourseModalOpen.value = false
|
|
||||||
} else {
|
|
||||||
toast.add({
|
|
||||||
title: '创建失败',
|
|
||||||
description: res.msg || '未知错误',
|
|
||||||
color: 'red',
|
|
||||||
icon: 'i-tabler-alert-triangle',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
creationPending.value = false
|
|
||||||
}).catch(e => {
|
|
||||||
creationPending.value = false
|
|
||||||
toast.add({
|
|
||||||
title: '创建失败',
|
|
||||||
description: e.message || '未知错误',
|
|
||||||
color: 'red',
|
|
||||||
icon: 'i-tabler-alert-triangle',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCourseDelete = (task_id: string) => {
|
|
||||||
if (!task_id) return
|
|
||||||
deletePending.value = true
|
|
||||||
useFetchWrapped<
|
|
||||||
req.gen.CourseGenDelete & AuthedRequest,
|
|
||||||
BaseResponse<resp.gen.CourseGenDelete>
|
|
||||||
>('App.Digital_Convert.Delete', {
|
|
||||||
token: loginState.token!,
|
|
||||||
user_id: loginState.user.id,
|
|
||||||
to_user_id: loginState.user.id,
|
|
||||||
task_id,
|
|
||||||
}).then(res => {
|
|
||||||
if (res.ret === 200) {
|
|
||||||
toast.add({
|
|
||||||
title: '删除成功',
|
|
||||||
description: '已删除任务记录',
|
|
||||||
color: 'green',
|
|
||||||
icon: 'i-tabler-check',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
toast.add({
|
|
||||||
title: '删除失败',
|
|
||||||
description: res.msg || '未知错误',
|
|
||||||
color: 'red',
|
|
||||||
icon: 'i-tabler-alert-triangle',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
deletePending.value = false
|
|
||||||
refreshCourseList()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const i = setInterval(refreshCourseList, 1000 * 5)
|
|
||||||
onBeforeUnmount(() => clearInterval(i))
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="font-sans h-full">
|
|
||||||
<div class="p-4 border-b dark:border-neutral-700">
|
|
||||||
<UButton
|
|
||||||
:trailing="false"
|
|
||||||
color="primary"
|
|
||||||
icon="i-tabler-plus"
|
|
||||||
label="新建微课"
|
|
||||||
size="md"
|
|
||||||
variant="solid"
|
|
||||||
@click="() => {
|
|
||||||
if (!loginState.is_logged_in) {
|
|
||||||
modal.open(ModalAuthentication)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onCreateCourseClick()
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Transition name="loading-screen">
|
|
||||||
<div v-if="!loginState.is_logged_in" class="w-full h-full">
|
|
||||||
<div class="w-full h-full flex flex-col justify-center items-center gap-2 bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<Icon name="i-tabler-user-circle" class="text-7xl text-neutral-300 dark:text-neutral-700"/>
|
|
||||||
<p class="text-sm text-neutral-500 dark:text-neutral-400">请登录后使用</p>
|
|
||||||
<UButton class="mt-2 font-bold" color="black" variant="solid" size="xs"
|
|
||||||
@click="modal.open(ModalAuthentication)">
|
|
||||||
登录
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="courseList?.length === 0"
|
|
||||||
class="w-full h-full flex flex-col justify-center items-center gap-2 bg-neutral-100 dark:bg-neutral-900">
|
|
||||||
<Icon name="i-tabler-photo-hexagon" class="text-7xl text-neutral-300 dark:text-neutral-700"/>
|
|
||||||
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
|
||||||
没有记录
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-else class="p-4">
|
|
||||||
<div class="relative grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4">
|
|
||||||
<TransitionGroup name="card">
|
|
||||||
<CGTaskCard
|
|
||||||
v-for="(course, index) in courseList"
|
|
||||||
:key="course.task_id || 'unknown' + index"
|
|
||||||
:course="course"
|
|
||||||
@delete="task_id => onCourseDelete(task_id)"
|
|
||||||
/>
|
|
||||||
</TransitionGroup>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<UModal
|
|
||||||
v-model="isCreateCourseModalOpen"
|
|
||||||
prevent-close
|
|
||||||
>
|
|
||||||
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
|
||||||
新建微课视频
|
|
||||||
</h3>
|
|
||||||
<UButton
|
|
||||||
class="-my-1"
|
|
||||||
color="gray"
|
|
||||||
icon="i-tabler-x"
|
|
||||||
variant="ghost"
|
|
||||||
@click="isCreateCourseModalOpen = false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<UForm
|
|
||||||
:schema="createCourseSchema"
|
|
||||||
:state="createCourseState"
|
|
||||||
class="space-y-4"
|
|
||||||
@submit="onCreateCourseSubmit"
|
|
||||||
>
|
|
||||||
<div class="flex justify-between gap-2 *:flex-1">
|
|
||||||
<UFormGroup label="微课标题" name="task_title">
|
|
||||||
<UInput v-model="createCourseState.task_title" placeholder="请输入微课标题"/>
|
|
||||||
</UFormGroup>
|
|
||||||
|
|
||||||
<UFormGroup label="生成线路" name="gen_server">
|
|
||||||
<USelectMenu
|
|
||||||
v-model="createCourseState.gen_server"
|
|
||||||
:options="[{
|
|
||||||
label: '主线路',
|
|
||||||
value: 'main',
|
|
||||||
}, {
|
|
||||||
label: '备用线路',
|
|
||||||
value: 'standby1',
|
|
||||||
}]"
|
|
||||||
option-attribute="label"
|
|
||||||
value-attribute="value"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UFormGroup :label="`视频倍速:${createCourseState.speed}`" name="speed">
|
|
||||||
<URange
|
|
||||||
v-model="createCourseState.speed"
|
|
||||||
:max="1.5"
|
|
||||||
:min="0.5"
|
|
||||||
:step="0.1"
|
|
||||||
class="pt-4"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
|
|
||||||
<UFormGroup label="PPT 文件">
|
|
||||||
<FileDnD
|
|
||||||
accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
||||||
@change="file => selected_file = file"
|
|
||||||
/>
|
|
||||||
</UFormGroup>
|
|
||||||
|
|
||||||
<div class="flex justify-end space-x-4 pt-4">
|
|
||||||
<UButton
|
|
||||||
color="gray"
|
|
||||||
label="取消"
|
|
||||||
variant="ghost"
|
|
||||||
@click="isCreateCourseModalOpen = false"
|
|
||||||
/>
|
|
||||||
<UButton
|
|
||||||
color="primary"
|
|
||||||
label="提交"
|
|
||||||
type="submit"
|
|
||||||
variant="solid"
|
|
||||||
:loading="creationPending"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</UForm>
|
|
||||||
</UCard>
|
|
||||||
</UModal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.loading-screen-leave-active {
|
|
||||||
@apply transition duration-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-screen-leave-to {
|
|
||||||
@apply opacity-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-enter-active, .card-leave-active {
|
|
||||||
transition: opacity 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-enter, .card-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-enter-to, .card-leave {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -40,7 +40,7 @@ const loadSrt = async () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: '加载字幕失败',
|
title: '加载字幕失败',
|
||||||
description: err as string || '未知错误',
|
description: `${ err }` || '未知错误',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
record: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const dayjs = useDayjs()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="relative w-full aspect-video rounded-lg overflow-hidden shadow"
|
|
||||||
>
|
|
||||||
<NuxtImg :src="record.video_cover"></NuxtImg>
|
|
||||||
<div class="absolute inset-0 flex flex-col justify-between bg-black/10 bg-gradient-to-t from-black/20">
|
|
||||||
<div class="flex justify-between items-start p-2.5 gap-2">
|
|
||||||
<div>
|
|
||||||
<UButton icon="i-solar-play-circle-bold-duotone">预览</UButton>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-end gap-1">
|
|
||||||
<UTooltip :text="record.task_id" :close-delay="300">
|
|
||||||
<h1 class="text-white text-xs font-bold font-sans">
|
|
||||||
ID: {{ record.task_id.slice(0, 6) }}
|
|
||||||
</h1>
|
|
||||||
</UTooltip>
|
|
||||||
<UTooltip :text="dayjs(record.create_time * 1000).format('YYYY-MM-DD HH:mm:ss')">
|
|
||||||
<h1 class="text-white text-xs font-bold font-sans">
|
|
||||||
{{ dayjs(record.create_time * 1000).fromNow() }}
|
|
||||||
</h1>
|
|
||||||
</UTooltip>
|
|
||||||
</div>
|
|
||||||
<!-- <UProgress-->
|
|
||||||
<!-- size="md"-->
|
|
||||||
<!-- indicator-->
|
|
||||||
<!-- :ui="{-->
|
|
||||||
<!-- wrapper: 'flex-col-reverse',-->
|
|
||||||
<!-- progress: {-->
|
|
||||||
<!-- base: '!bg-opacity-50'-->
|
|
||||||
<!-- }-->
|
|
||||||
<!-- }"-->
|
|
||||||
<!-- :value="10"-->
|
|
||||||
<!-- :max="100"-->
|
|
||||||
<!-- :animation="'carousel'"-->
|
|
||||||
<!-- />-->
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between items-center p-2.5 gap-2">
|
|
||||||
<div class="overflow-hidden whitespace-nowrap">
|
|
||||||
<UTooltip
|
|
||||||
:text="record.title"
|
|
||||||
:popper="{
|
|
||||||
placement: 'bottom-start'
|
|
||||||
}"
|
|
||||||
:open-delay="300"
|
|
||||||
:close-delay="300"
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
<h1 class="text-white text-base font-bold font-sans drop-shadow overflow-hidden text-ellipsis leading-none">
|
|
||||||
{{ record.title }}
|
|
||||||
</h1>
|
|
||||||
</UTooltip>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 whitespace-nowrap flex gap-1.5">
|
|
||||||
<UButton
|
|
||||||
size="xs"
|
|
||||||
color="red"
|
|
||||||
variant="soft"
|
|
||||||
icon="i-tabler-trash"
|
|
||||||
/>
|
|
||||||
<UButton
|
|
||||||
size="xs"
|
|
||||||
color="primary"
|
|
||||||
variant="soft"
|
|
||||||
icon="i-solar-subtitles-linear"
|
|
||||||
>
|
|
||||||
字幕
|
|
||||||
</UButton>
|
|
||||||
<UButton
|
|
||||||
size="xs"
|
|
||||||
icon="i-tabler-download"
|
|
||||||
>
|
|
||||||
下载
|
|
||||||
</UButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,24 +1,28 @@
|
|||||||
export const fetchCourseSubtitleUrl = async (course: resp.gen.CourseGenItem) => {
|
export const fetchCourseSubtitleUrl = async (course: resp.gen.CourseGenItem) => {
|
||||||
const loginState = useLoginState()
|
const loginState = useLoginState()
|
||||||
|
|
||||||
const subtitleRecord = await useFetchWrapped<
|
try {
|
||||||
{
|
const subtitleRecord = await useFetchWrapped<
|
||||||
page?: number
|
{
|
||||||
perpage?: number
|
page?: number
|
||||||
task_id: string
|
perpage?: number
|
||||||
} & AuthedRequest,
|
task_id: string
|
||||||
BaseResponse<PagedData<resp.gen.CourseSubtitleCreate>>
|
} & AuthedRequest,
|
||||||
>('App.Digital_VideoSubtitle.GetList', {
|
BaseResponse<PagedData<resp.gen.CourseSubtitleCreate>>
|
||||||
token: loginState.token!,
|
>('App.Digital_VideoSubtitle.GetList', {
|
||||||
user_id: loginState.user.id,
|
token: loginState.token!,
|
||||||
task_id: course.task_id,
|
user_id: loginState.user.id,
|
||||||
page: 1,
|
task_id: course.task_id,
|
||||||
perpage: 1,
|
page: 1,
|
||||||
})
|
perpage: 1,
|
||||||
|
})
|
||||||
|
|
||||||
if (subtitleRecord.data.items.length !== 1) {
|
if (subtitleRecord.data.items.length !== 1) {
|
||||||
|
return course.subtitle_url
|
||||||
|
}
|
||||||
|
|
||||||
|
return subtitleRecord.data.items[0].url
|
||||||
|
} catch (err) {
|
||||||
return course.subtitle_url
|
return course.subtitle_url
|
||||||
}
|
}
|
||||||
|
|
||||||
return subtitleRecord.data.items[0].url
|
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ const links = [
|
|||||||
}, {
|
}, {
|
||||||
label: 'PPT',
|
label: 'PPT',
|
||||||
icon: 'i-tabler-file-type-ppt',
|
icon: 'i-tabler-file-type-ppt',
|
||||||
to: '/aigc/course-generate',
|
to: '/aigc/generation',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ const open_login_modal = () => {
|
|||||||
<header>
|
<header>
|
||||||
<h1 class="inline-flex flex-col">
|
<h1 class="inline-flex flex-col">
|
||||||
<span class="text-lg text-neutral-600 dark:text-neutral-300 font-bold">眩生花 AI 助手</span>
|
<span class="text-lg text-neutral-600 dark:text-neutral-300 font-bold">眩生花 AI 助手</span>
|
||||||
<span class="text-xs text-neutral-600 dark:text-neutral-300">这里可以有一个副标题</span>
|
<span class="text-xs text-neutral-600 dark:text-neutral-300">XSH AI Assistant</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="hidden md:block">
|
<div class="hidden md:block">
|
||||||
<UHorizontalNavigation :links="links" class="select-none"/>
|
<UHorizontalNavigation :links="links" class="select-none"/>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export default defineNuxtConfig({
|
|||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
API_BASE: 'https://service1.fenshenzhike.com/',
|
API_BASE: 'https://service2.fenshenzhike.com/',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
"@iconify-json/tabler": "^1.1.118",
|
"@iconify-json/tabler": "^1.1.118",
|
||||||
"@monosky/base64": "^0.0.3",
|
"@monosky/base64": "^0.0.3",
|
||||||
"@nuxt/image": "^1.7.0",
|
"@nuxt/image": "^1.7.0",
|
||||||
"@nuxt/ui": "^2.18.2",
|
|
||||||
"@uniiem/object-trim": "^0.2.0",
|
"@uniiem/object-trim": "^0.2.0",
|
||||||
"@uniiem/uuid": "^0.2.1",
|
"@uniiem/uuid": "^0.2.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
@@ -32,6 +31,7 @@
|
|||||||
"yup": "^1.4.0"
|
"yup": "^1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@nuxt/ui": "^2.18.3",
|
||||||
"@nuxtjs/google-fonts": "^3.2.0",
|
"@nuxtjs/google-fonts": "^3.2.0",
|
||||||
"@pinia-plugin-persistedstate/nuxt": "^1.2.1",
|
"@pinia-plugin-persistedstate/nuxt": "^1.2.1",
|
||||||
"@pinia/nuxt": "^0.5.2",
|
"@pinia/nuxt": "^0.5.2",
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
"sass": "^1.77.8"
|
"sass": "^1.77.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"dayjs": "^1.11.12"
|
"dayjs": "^1.11.12",
|
||||||
|
"tailwindcss": "^3.4.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
<script lang="ts" setup>
|
|
||||||
import CourseGenerate from '~/components/aigc/course-generate/CourseGenerate.vue'
|
|
||||||
import CourseGenerateGreenScreen from '~/components/aigc/course-generate/CourseGenerateGreenScreen.vue'
|
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: 'PPT 生成视频 | XSH AI',
|
|
||||||
})
|
|
||||||
|
|
||||||
const showSidebar = ref(false)
|
|
||||||
const toggleSidebar = () => {
|
|
||||||
showSidebar.value = !showSidebar.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
name: '微课视频生成',
|
|
||||||
component: CourseGenerate,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '绿幕视频生成',
|
|
||||||
component: CourseGenerateGreenScreen,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const currentComponentIndex = ref(0)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="w-full flex relative">
|
|
||||||
<div
|
|
||||||
:class="{'translate-x-0': showSidebar}"
|
|
||||||
class="absolute -translate-x-full md:sticky md:translate-x-0 z-10 flex flex-col h-[calc(100vh-4rem)] bg-neutral-100 dark:bg-neutral-900 p-4 w-full md:w-[300px]
|
|
||||||
shadow-sidebar border-r border-transparent dark:border-neutral-700 transition-all duration-300 ease-out"
|
|
||||||
>
|
|
||||||
<div class="flex-1 flex flex-col overflow-auto overflow-x-hidden">
|
|
||||||
<div class="flex flex-col gap-3 relative">
|
|
||||||
<div class="space-y-2 nav">
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="(route, index) in routes"
|
|
||||||
:key="index"
|
|
||||||
:class="['nav-item', { active: currentComponentIndex === index }]"
|
|
||||||
@click="currentComponentIndex = index"
|
|
||||||
>
|
|
||||||
{{ route.name }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="h-[calc(100vh-4rem)] flex-1 overflow-y-auto bg-white dark:bg-neutral-900">
|
|
||||||
<ClientOnly>
|
|
||||||
<Component :is="routes[currentComponentIndex].component"/>
|
|
||||||
</ClientOnly>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.nav {
|
|
||||||
&-item {
|
|
||||||
@apply w-full px-4 py-3 bg-transparent rounded-md cursor-pointer select-none;
|
|
||||||
@apply transition duration-150 ease-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
@apply bg-black/10;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
@apply bg-primary/20 text-primary shadow shadow-primary/20 font-medium;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,9 +1,31 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import NavItem from '~/components/ppt/NavItem.vue'
|
import NavItem from '~/components/aigc/NavItem.vue'
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: '智能生成',
|
title: '智能生成',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const navList = [
|
||||||
|
{
|
||||||
|
label: '微课视频生成',
|
||||||
|
icon: 'tabler:presentation-analytics',
|
||||||
|
to: '/aigc/generation/course',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '绿幕视频生成',
|
||||||
|
icon: 'i-tabler-video',
|
||||||
|
to: '/aigc/generation/green-screen',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (route.fullPath === '/aigc/generation') {
|
||||||
|
router.push('/aigc/generation/course')
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -15,14 +37,11 @@ useSeoMeta({
|
|||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<NavItem
|
<NavItem
|
||||||
icon="tabler:presentation-analytics"
|
v-for="(item, i) in navList"
|
||||||
label="微课视频生成"
|
:key="i"
|
||||||
to="/aigc/generation/video-generate"
|
:icon="item.icon"
|
||||||
/>
|
:label="item.label"
|
||||||
<NavItem
|
:to="item.to"
|
||||||
icon="tabler:user-screen"
|
|
||||||
label="数字讲师"
|
|
||||||
to="/aigc/generation/digital-teachers"
|
|
||||||
/>
|
/>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,10 +49,14 @@ useSeoMeta({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LoginNeededContent
|
<LoginNeededContent
|
||||||
content-class="h-[calc(100vh-4rem)] flex-1 bg-white dark:bg-neutral-900"
|
content-class="h-[calc(100vh-4rem)] flex-1 overflow-y-auto bg-white dark:bg-neutral-900"
|
||||||
>
|
>
|
||||||
<Transition name="subpage" mode="out-in">
|
<Transition name="subpage" mode="out-in">
|
||||||
<NuxtPage :page-key="route => route.fullPath"/>
|
<div>
|
||||||
|
<Suspense>
|
||||||
|
<NuxtPage :page-key="route.fullPath" keepalive/>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</LoginNeededContent>
|
</LoginNeededContent>
|
||||||
|
|
||||||
@@ -43,11 +66,11 @@ useSeoMeta({
|
|||||||
<style>
|
<style>
|
||||||
.subpage-enter-active,
|
.subpage-enter-active,
|
||||||
.subpage-leave-active {
|
.subpage-leave-active {
|
||||||
transition: opacity 0.3s;
|
@apply transition-all duration-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subpage-enter-from,
|
.subpage-enter-from,
|
||||||
.subpage-leave-to {
|
.subpage-leave-to {
|
||||||
opacity: 0;
|
@apply opacity-0 translate-x-4;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
170
pages/aigc/generation/course.vue
Normal file
170
pages/aigc/generation/course.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import CGTaskCard from '~/components/aigc/course-generate/CGTaskCard.vue'
|
||||||
|
import { useFetchWrapped } from '~/composables/useFetchWrapped'
|
||||||
|
import ModalAuthentication from '~/components/ModalAuthentication.vue'
|
||||||
|
import SlideCreateCourse from '~/components/SlideCreateCourse.vue'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const modal = useModal()
|
||||||
|
const slide = useSlideover()
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
|
const deletePending = ref(false)
|
||||||
|
const page = ref(1)
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: courseList,
|
||||||
|
refresh: refreshCourseList,
|
||||||
|
} = useAsyncData(
|
||||||
|
() => useFetchWrapped<
|
||||||
|
req.gen.CourseGenList & AuthedRequest,
|
||||||
|
BaseResponse<PagedData<resp.gen.CourseGenItem>>
|
||||||
|
>('App.Digital_Convert.GetList', {
|
||||||
|
token: loginState.token!,
|
||||||
|
user_id: loginState.user.id,
|
||||||
|
to_user_id: loginState.user.id,
|
||||||
|
page: page.value,
|
||||||
|
perpage: 16,
|
||||||
|
}), {
|
||||||
|
watch: [page],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const onCreateCourseClick = () => {
|
||||||
|
slide.open(SlideCreateCourse, {
|
||||||
|
onSuccess: () => {
|
||||||
|
refreshCourseList()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCourseDelete = (task_id: string) => {
|
||||||
|
if (!task_id) return
|
||||||
|
deletePending.value = true
|
||||||
|
useFetchWrapped<
|
||||||
|
req.gen.CourseGenDelete & AuthedRequest,
|
||||||
|
BaseResponse<resp.gen.CourseGenDelete>
|
||||||
|
>('App.Digital_Convert.Delete', {
|
||||||
|
token: loginState.token!,
|
||||||
|
user_id: loginState.user.id,
|
||||||
|
to_user_id: loginState.user.id,
|
||||||
|
task_id,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ret === 200) {
|
||||||
|
toast.add({
|
||||||
|
title: '删除成功',
|
||||||
|
description: '已删除任务记录',
|
||||||
|
color: 'green',
|
||||||
|
icon: 'i-tabler-check',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
title: '删除失败',
|
||||||
|
description: res.msg || '未知错误',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
deletePending.value = false
|
||||||
|
refreshCourseList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeLeave = (el: any) => {
|
||||||
|
el.style.width = `${ el.offsetWidth }px`
|
||||||
|
el.style.height = `${ el.offsetHeight }px`
|
||||||
|
}
|
||||||
|
|
||||||
|
const leave = (el: any, done: Function) => {
|
||||||
|
el.style.position = 'absolute'
|
||||||
|
el.style.transition = 'none' // 取消过渡动画
|
||||||
|
el.style.opacity = 0 // 立即隐藏元素
|
||||||
|
done()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const i = setInterval(refreshCourseList, 1000 * 5)
|
||||||
|
onBeforeUnmount(() => clearInterval(i))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="font-sans h-full">
|
||||||
|
<div class="p-4 pb-0">
|
||||||
|
<BubbleTitle subtitle="VIDEOS" title="我的微课视频">
|
||||||
|
<template #action>
|
||||||
|
<UButton
|
||||||
|
:trailing="false"
|
||||||
|
color="primary"
|
||||||
|
icon="i-tabler-plus"
|
||||||
|
label="新建微课"
|
||||||
|
size="md"
|
||||||
|
variant="solid"
|
||||||
|
@click="() => {
|
||||||
|
if (!loginState.is_logged_in) {
|
||||||
|
modal.open(ModalAuthentication)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onCreateCourseClick()
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</BubbleTitle>
|
||||||
|
<GradientDivider/>
|
||||||
|
</div>
|
||||||
|
<Transition name="loading-screen">
|
||||||
|
<div v-if="courseList?.data.items.length === 0"
|
||||||
|
class="w-full h-full flex flex-col justify-center items-center gap-2 bg-neutral-100 dark:bg-neutral-900">
|
||||||
|
<Icon class="text-7xl text-neutral-300 dark:text-neutral-700" name="i-tabler-photo-hexagon"/>
|
||||||
|
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||||
|
没有记录
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="p-4">
|
||||||
|
<div class="relative grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 fhd:grid-cols-5 gap-4">
|
||||||
|
<TransitionGroup
|
||||||
|
name="card"
|
||||||
|
@beforeLeave="beforeLeave"
|
||||||
|
@leave="leave"
|
||||||
|
>
|
||||||
|
<CGTaskCard
|
||||||
|
v-for="(course, index) in courseList?.data.items"
|
||||||
|
:key="course.task_id || 'unknown' + index"
|
||||||
|
:course="course"
|
||||||
|
@delete="task_id => onCourseDelete(task_id)"
|
||||||
|
/>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<UPagination v-model="page" :page-count="16" :total="courseList?.data.total || 0"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-screen-leave-active {
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-screen-leave-to {
|
||||||
|
@apply opacity-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-move,
|
||||||
|
.card-enter-active,
|
||||||
|
.card-leave-active {
|
||||||
|
@apply transition-all duration-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-enter-from,
|
||||||
|
.card-leave-to {
|
||||||
|
@apply opacity-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-leave-active {
|
||||||
|
@apply absolute;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
useSeoMeta({
|
|
||||||
title: '数字化教师',
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="p-4">
|
|
||||||
<BubbleTitle title="数字化讲师" subtitle="DIGITAL" />
|
|
||||||
<GradientDivider />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
useSeoMeta({
|
|
||||||
title: '课程视频生成',
|
|
||||||
})
|
|
||||||
|
|
||||||
const testItem = {
|
|
||||||
'id': 1599,
|
|
||||||
'device_id': 'Test_Device_V3',
|
|
||||||
'user_id': 1,
|
|
||||||
'task_id': 'SQOeN1j2heRoQeGGTFh3Tu2WP9kUcz4L',
|
|
||||||
'create_time': 1713408239,
|
|
||||||
'token': 'not use',
|
|
||||||
'progress': 100,
|
|
||||||
'digital_human_id': 40696,
|
|
||||||
'complete_time': 1713409821,
|
|
||||||
'duration': 1578819,
|
|
||||||
'video_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/75d1e1cee595a7f5758c59289d1a74b9.mp4',
|
|
||||||
'subtitle_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/a95cfef4524e90f5509a5c248e5c2061.srt',
|
|
||||||
'video_cover': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/b4161a85573fc09be82fa7cf7dd9abfa.png',
|
|
||||||
'custom_video': '[]',
|
|
||||||
'title': '1-2 一键启动:零基础快速搭建Keil开发环境实操教程',
|
|
||||||
'ppt_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/material/2024-04-18/0a8827a1ae32ece196536a19bab1dff5.pptx',
|
|
||||||
'opening_url': '',
|
|
||||||
'ending_url': '',
|
|
||||||
'video_duration': 507,
|
|
||||||
'message': 'ok',
|
|
||||||
'speed': 1,
|
|
||||||
}
|
|
||||||
const testItem2 = {
|
|
||||||
'id': 1599,
|
|
||||||
'device_id': 'Test_Device_V3',
|
|
||||||
'user_id': 1,
|
|
||||||
'task_id': 'SQOeN1j2heRoQeGGTFh3Tu2WP9kUcz4L',
|
|
||||||
'create_time': 1713408239,
|
|
||||||
'token': 'not use',
|
|
||||||
'progress': null,
|
|
||||||
'digital_human_id': 40696,
|
|
||||||
'complete_time': 1713409821,
|
|
||||||
'duration': 1578819,
|
|
||||||
'video_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/75d1e1cee595a7f5758c59289d1a74b9.mp4',
|
|
||||||
'subtitle_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/a95cfef4524e90f5509a5c248e5c2061.srt',
|
|
||||||
'video_cover': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/b4161a85573fc09be82fa7cf7dd9abfa.png',
|
|
||||||
'custom_video': '[]',
|
|
||||||
'title': '1-2 一键启动:零基础快速搭建Keil开发环境实操教程',
|
|
||||||
'ppt_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/material/2024-04-18/0a8827a1ae32ece196536a19bab1dff5.pptx',
|
|
||||||
'opening_url': '',
|
|
||||||
'ending_url': '',
|
|
||||||
'video_duration': 507,
|
|
||||||
'message': 'ok',
|
|
||||||
'speed': 1,
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="p-4">
|
|
||||||
<BubbleTitle title="我的微课" subtitle="VIDEOS"/>
|
|
||||||
<GradientDivider/>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
|
||||||
<PPTGenerationRecord :record="testItem"/>
|
|
||||||
<PPTGenerationRecord :record="testItem2"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -26,9 +26,6 @@ importers:
|
|||||||
'@nuxt/image':
|
'@nuxt/image':
|
||||||
specifier: ^1.7.0
|
specifier: ^1.7.0
|
||||||
version: 1.7.0(idb-keyval@6.2.1)(ioredis@5.4.1)(magicast@0.3.4)(rollup@4.19.1)
|
version: 1.7.0(idb-keyval@6.2.1)(ioredis@5.4.1)(magicast@0.3.4)(rollup@4.19.1)
|
||||||
'@nuxt/ui':
|
|
||||||
specifier: ^2.18.2
|
|
||||||
version: 2.18.2(idb-keyval@6.2.1)(magicast@0.3.4)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)
|
|
||||||
'@uniiem/object-trim':
|
'@uniiem/object-trim':
|
||||||
specifier: ^0.2.0
|
specifier: ^0.2.0
|
||||||
version: 0.2.0
|
version: 0.2.0
|
||||||
@@ -59,6 +56,9 @@ importers:
|
|||||||
radix-vue:
|
radix-vue:
|
||||||
specifier: ^1.9.2
|
specifier: ^1.9.2
|
||||||
version: 1.9.2(vue@3.4.34)
|
version: 1.9.2(vue@3.4.34)
|
||||||
|
tailwindcss:
|
||||||
|
specifier: ^3.4.7
|
||||||
|
version: 3.4.7
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.4.34
|
specifier: ^3.4.34
|
||||||
version: 3.4.34
|
version: 3.4.34
|
||||||
@@ -69,6 +69,9 @@ importers:
|
|||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
version: 1.4.0
|
version: 1.4.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@nuxt/ui':
|
||||||
|
specifier: ^2.18.3
|
||||||
|
version: 2.18.3(idb-keyval@6.2.1)(magicast@0.3.4)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)
|
||||||
'@nuxtjs/google-fonts':
|
'@nuxtjs/google-fonts':
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
version: 3.2.0(magicast@0.3.4)(rollup@4.19.1)
|
version: 3.2.0(magicast@0.3.4)(rollup@4.19.1)
|
||||||
@@ -1316,8 +1319,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: '*'
|
vite: '*'
|
||||||
|
|
||||||
'@nuxt/icon@1.3.1':
|
'@nuxt/icon@1.4.4':
|
||||||
resolution: {integrity: sha512-bpue1uzVTx3Fiwb0625ShaY5SFio8vcZ3L4kIb2tBc/BbENASQcCa27uk87zuWFJdnRWQbNpn/L4wc+wKZ0Wbw==}
|
resolution: {integrity: sha512-XjpW6dO98y+7sxyhhAeka96cFdbThWEr6tgazVQ2d/RRgg9/9aYctcxo4GCoSD6APXf0k9gQE7WtiIIUKrjr3Q==}
|
||||||
|
|
||||||
'@nuxt/image@1.7.0':
|
'@nuxt/image@1.7.0':
|
||||||
resolution: {integrity: sha512-zSj32bLgbV9AvLkLX0pF52J5KBfSyj0eSIdpXCtTJATSZlqgcJigoCvmabC1nbcMIp0SZ29Bu9+acQpGTQKz+g==}
|
resolution: {integrity: sha512-zSj32bLgbV9AvLkLX0pF52J5KBfSyj0eSIdpXCtTJATSZlqgcJigoCvmabC1nbcMIp0SZ29Bu9+acQpGTQKz+g==}
|
||||||
@@ -1335,8 +1338,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-KH6wxzsNys69daSO0xUv0LEBAfhwwjK1M+0Cdi1/vxmifCslMIY7lN11B4eywSfscbyVPAYJvANyc7XiVPImBQ==}
|
resolution: {integrity: sha512-KH6wxzsNys69daSO0xUv0LEBAfhwwjK1M+0Cdi1/vxmifCslMIY7lN11B4eywSfscbyVPAYJvANyc7XiVPImBQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@nuxt/ui@2.18.2':
|
'@nuxt/ui@2.18.3':
|
||||||
resolution: {integrity: sha512-azij3fV1FkEMG+sgTMEQiXkJMnII3HNQE+3r8g9QhUgO7iZ13TlUkXl0iYd+U/cGa4aZYki6WXJ45WyETtV/IA==}
|
resolution: {integrity: sha512-bFy3K0cDPRlEYclQAjIgVFUGFb91sGYAtBmEzunFux85aUw0g4tI7vsWdyVijhKzGOyOjBuLGaG40kGl4AIXpA==}
|
||||||
engines: {node: '>=v16.20.2'}
|
engines: {node: '>=v16.20.2'}
|
||||||
|
|
||||||
'@nuxt/vite-builder@3.12.4':
|
'@nuxt/vite-builder@3.12.4':
|
||||||
@@ -6354,7 +6357,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
'@nuxt/icon@1.3.1(magicast@0.3.4)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)':
|
'@nuxt/icon@1.4.4(magicast@0.3.4)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/collections': 1.0.443
|
'@iconify/collections': 1.0.443
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
@@ -6365,6 +6368,7 @@ snapshots:
|
|||||||
consola: 3.2.3
|
consola: 3.2.3
|
||||||
fast-glob: 3.3.2
|
fast-glob: 3.3.2
|
||||||
local-pkg: 0.5.0
|
local-pkg: 0.5.0
|
||||||
|
mlly: 1.7.1
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
@@ -6475,12 +6479,12 @@ snapshots:
|
|||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@nuxt/ui@2.18.2(idb-keyval@6.2.1)(magicast@0.3.4)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)':
|
'@nuxt/ui@2.18.3(idb-keyval@6.2.1)(magicast@0.3.4)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@headlessui/tailwindcss': 0.2.1(tailwindcss@3.4.7)
|
'@headlessui/tailwindcss': 0.2.1(tailwindcss@3.4.7)
|
||||||
'@headlessui/vue': 1.7.22(vue@3.4.34)
|
'@headlessui/vue': 1.7.22(vue@3.4.34)
|
||||||
'@iconify-json/heroicons': 1.1.23
|
'@iconify-json/heroicons': 1.1.23
|
||||||
'@nuxt/icon': 1.3.1(magicast@0.3.4)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)
|
'@nuxt/icon': 1.4.4(magicast@0.3.4)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(vue@3.4.34)
|
||||||
'@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@4.19.1)
|
'@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@4.19.1)
|
||||||
'@nuxtjs/color-mode': 3.4.2(magicast@0.3.4)(rollup@4.19.1)
|
'@nuxtjs/color-mode': 3.4.2(magicast@0.3.4)(rollup@4.19.1)
|
||||||
'@nuxtjs/tailwindcss': 6.12.1(magicast@0.3.4)(rollup@4.19.1)
|
'@nuxtjs/tailwindcss': 6.12.1(magicast@0.3.4)(rollup@4.19.1)
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ export default <Partial<Config>>{
|
|||||||
sans: ['Rubik', 'Noto Sans SC', 'sans-serif'],
|
sans: ['Rubik', 'Noto Sans SC', 'sans-serif'],
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
|
screens: {
|
||||||
|
'hd': '1280px',
|
||||||
|
'fhd': '1920px',
|
||||||
|
'2k': '2560px',
|
||||||
|
'4k': '3840px',
|
||||||
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
'2xs': '0.625rem',
|
'2xs': '0.625rem',
|
||||||
},
|
},
|
||||||
|
|||||||
18
typings/types.d.ts
vendored
18
typings/types.d.ts
vendored
@@ -28,6 +28,18 @@ interface UserSchema {
|
|||||||
auth_code: 0 | 1 | 2 // 0: Banned, 1: User, 2: Operator
|
auth_code: 0 | 1 | 2 // 0: Banned, 1: User, 2: Operator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DigitalHumanItem {
|
||||||
|
user_id: number
|
||||||
|
create_time: number
|
||||||
|
model_id: number
|
||||||
|
avatar: string
|
||||||
|
type: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
id?: number
|
||||||
|
digital_human_id: number
|
||||||
|
}
|
||||||
|
|
||||||
// Common request and response schemas
|
// Common request and response schemas
|
||||||
namespace req {
|
namespace req {
|
||||||
namespace user {
|
namespace user {
|
||||||
@@ -97,6 +109,12 @@ namespace req {
|
|||||||
sub_content: string
|
sub_content: string
|
||||||
sub_ver?: number
|
sub_ver?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DigitalHumanList {
|
||||||
|
to_user_id: number
|
||||||
|
page?: number
|
||||||
|
perpage?: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AssistantTemplateList {
|
interface AssistantTemplateList {
|
||||||
|
|||||||
Reference in New Issue
Block a user