Commit 66c8a111 authored by nick zheng's avatar nick zheng Committed by Dazzle Wu

feat: 我的对话及对话配置

parent 87f7faa7
......@@ -21,6 +21,7 @@
"@unocss/reset": "^0.61.9",
"@vueuse/core": "^10.11.1",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
"nanoid": "^5.0.7",
"pinia": "^2.2.2",
"spark-md5": "^3.0.2",
......
This diff is collapsed.
import { request } from '@/utils/request'
// 通过configId获取对话配置
export function fetchGetDigitalHumanDialogueConfigByConfigId<T>(configId: string) {
return request.post<T>(`/bizDigitalHumanDialogueConfigRest/getByConfigId.json?configId=${configId}`)
}
// 保存更新对话配置
export function fetchSaveDigitalHumanDialogueConfig<T>(payload: object) {
return request.post<T>('/bizDigitalHumanDialogueConfigRest/saveOrUpdate.json', payload)
}
// 通过configId删除对话配置
export function fetchDelectDigitalHumanDialogueConfigByConfigId<T>(configId: string) {
return request.post<T>(`/bizDigitalHumanDialogueConfigRest/deletedByConfigId.json?configId=${configId}`)
}
// 批量删除对话配置
export function fetchMultiDelectDigitalHumanDialogueConfig<T>(payload: string[]) {
return request.post<T>('/bizDigitalHumanDialogueConfigRest/deletedByConfigIds.json', payload)
}
// 通过对话数字人列表
export function fetchGetDigitalHumanDialogueList<T>(query: string) {
return request.post<T>(`/bizDigitalHumanDialogueConfigRest/getList.json?query=${query || null}`)
}
// 更改对话数字人开关配置
export function fetchUpdateDigitalHumanDialogueOpen<T>(configId: string, isOpen: 'Y' | 'N') {
return request.post<T>(`/bizDigitalHumanDialogueConfigRest/openConfig.json?configId=${configId}&isOpen=${isOpen}`)
}
// 获取角色模型列表
export function fetchDigitalHumanDialogueSystemList<T>(payload: object) {
return request.post<T>('/bizDigitalHumanDialogueSystemModelRest/getList.json', payload)
}
// 根据figureId获取数字人详情
export function fetchGetInfoByFigureId<T>(figureId: string) {
return request.post<T>(`/bizDigitalHumanImageRest/getInfoByFigureId.json?figureId=${figureId}`)
}
// 获取当前用户背景图
export function fetchGetBackgroundImageList<T>() {
return request.post<T>('/aiDigitalHumanImageRest/getBackgroundImageList.json')
}
<script setup lang="ts">
import { computed, useSlots } from 'vue'
import { modalHeaderStyle, modalContentStyle, modalFooterStyle } from './modal-style'
interface Props {
title: string // 弹窗标题
isShow: boolean // 是否显示
height?: number // 高度
width?: number // 宽度
borderRadius?: number // 圆角
btnLoading?: boolean // 按钮是否加载中
btnDisabled?: boolean // 按钮是否禁用
cancelBtnText?: string // 取消按钮文字
confirmBtnText?: string // 取消按钮文字
labelWidth?: number // 标签的宽度
labelPlacement?: 'left' | 'top' // 标签显示的位置
}
interface Emits {
(e: 'update:isShow', value: boolean): void
(e: 'close'): void
(e: 'confirm'): void
}
const props = withDefaults(defineProps<Props>(), {
height: 240,
width: 500,
borderRadius: 6,
btnLoading: false,
btnDisabled: false,
cancelBtnText: '取 消',
confirmBtnText: '確 認',
labelWidth: 80,
labelPlacement: 'left',
})
const emit = defineEmits<Emits>()
const slots = useSlots()
const modalBasicStyle = {
width: props.width + 'px',
minHeight: props.height + 'px',
borderRadius: props.borderRadius + 'px',
}
const showModal = computed({
get() {
return props.isShow
},
set(value: boolean) {
emit('update:isShow', value)
},
})
function handleCloseModal() {
showModal.value = false
emit('close')
}
function handleDetele() {
emit('confirm')
}
</script>
<template>
<NModal
preset="card"
transform-origin="center"
closable
:style="modalBasicStyle"
:show="showModal"
:bordered="false"
:auto-focus="false"
:header-style="modalHeaderStyle"
:content-style="modalContentStyle"
:footer-style="modalFooterStyle"
:on-mask-click="handleCloseModal"
@close="handleCloseModal"
>
<template #header>
<div class="text-base">{{ title }}</div>
</template>
<div>
<slot name="content" />
</div>
<template #footer>
<slot v-if="slots.footer" name="footer" />
<div v-else class="flex w-full items-center justify-end">
<NButton class="h-[32px]! rounded-md! px-6!" @click="handleCloseModal"> {{ cancelBtnText }} </NButton>
<NButton
:loading="btnLoading"
type="info"
:disabled="btnDisabled"
class="h-[32px]! px-5! rounded-md! ml-4!"
@click="handleDetele"
>
{{ confirmBtnText }}
</NButton>
</div>
</template>
</NModal>
</template>
export const modalHeaderStyle = {
padding: '24px 24px 16px',
fontSize: '16px',
}
export const modalContentStyle = {
padding: '0 24px 24px',
overflow: 'auto',
}
export const modalFooterStyle = {
padding: '0px 24px 24px 24px',
}
<script setup lang="ts">
export interface PaginationInfo {
pageNo: number
pageSize: number
totalPages: number
totalRows: number
}
interface Props {
pagingInfo: PaginationInfo
}
const pageSizes = [
{
label: '10 / 每頁',
value: 10,
},
{
label: '20 / 每頁',
value: 20,
},
{
label: '30 / 每頁',
value: 30,
},
{
label: '40 / 每頁',
value: 40,
},
]
interface Emits {
(e: 'update:pagingInfo', value: PaginationInfo): void
(e: 'updatePageNo', value: number): void
(e: 'updatePageSize', value: number): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
async function handleUpdatePageNo(pageNo: number) {
emit('updatePageNo', pageNo)
}
async function handleUpdatePageSize(pageSize: number) {
emit('updatePageSize', pageSize)
}
</script>
<template>
<div class="flex items-center">
<span class="text-[#999]">{{ pagingInfo.totalRows }}</span>
<NPagination
class="custom-pagination"
:page="pagingInfo.pageNo"
:page-count="pagingInfo.totalPages"
:page-sizes="pageSizes"
size="medium"
show-quick-jumper
show-size-picker
@update:page="handleUpdatePageNo"
@update:page-size="handleUpdatePageSize"
/>
<span class="ml-[10px] text-[#999]"></span>
</div>
</template>
<style lang="scss" scoped>
:deep(.custom-pagination .n-pagination-item) {
padding: 0;
margin-left: 2px;
border-radius: 5px;
}
:deep(.custom-pagination .n-pagination-item--button) {
background-color: white !important;
border: none !important;
}
:deep(.custom-pagination .n-base-selection) {
width: 100px;
border-radius: 6px;
--n-border: 1px solid #999 !important;
}
:deep(.custom-pagination .n-pagination-quick-jumper),
:deep(.custom-pagination .n-input__input-el),
:deep(.custom-pagination .n-base-selection-input__content) {
color: #999;
}
:deep(.custom-pagination .n-input) {
border-radius: 6px;
--n-border: 1px solid #999 !important;
}
</style>
import { onMounted, nextTick, onUnmounted, ref, Ref, computed } from 'vue'
function debounce(handler: CallableFunction, delay: number) {
let timer: number | undefined = undefined
return () => {
clearTimeout(timer)
timer = setTimeout(handler, delay || 0)
}
}
export default function useTableScrollY(unavailableHeight = 0) {
const pageContentWrapRef = ref<HTMLElement | null>(null)
const pageContentWrapOffsetHeight = ref(0)
const tableContentY = computed(() => {
// 页面高度 - 不可用的高度
return pageContentWrapOffsetHeight.value - unavailableHeight
})
const updatePageContentWrapOffsetHeightWithDebounce = debounce(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
}, 100)
onMounted(() => {
nextTick(() => {
setTimeout(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
setTimeout(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
setTimeout(() => {
updatePageContentWrapOffsetHeight(pageContentWrapRef, pageContentWrapOffsetHeight)
}, 200)
}, 200)
}, 200)
})
window.addEventListener('resize', updatePageContentWrapOffsetHeightWithDebounce)
})
onUnmounted(() => {
window.removeEventListener('resize', updatePageContentWrapOffsetHeightWithDebounce)
})
function updatePageContentWrapOffsetHeight(
pageContentWrapRef: Ref<HTMLElement | null>,
pageContentWrapOffsetHeight: Ref<number>,
) {
if (pageContentWrapRef.value) {
pageContentWrapOffsetHeight.value = pageContentWrapRef.value.offsetHeight || 0
}
}
return {
tableContentY,
pageContentWrapRef,
}
}
......@@ -2,3 +2,8 @@ export const BASE_URLS: Record<'DEV' | 'PROD', string> = {
DEV: 'https://digitalperson-sit.gsstcloud.com',
PROD: 'https://digitalperson-sit.gsstcloud.com',
}
export const AI_INDEX_URLS: Record<'DEV' | 'PROD', string> = {
DEV: 'https://ai-sit.gsstcloud.com/#/',
PROD: 'https://ai.gsstcloud.com/#/',
}
import { type RouteRecordRaw } from 'vue-router'
import Index from '@/views/index/index.vue'
export default [
{
path: '/dialogue-detail/:configId?',
name: 'DialogueDetail',
meta: {
rank: 1001,
title: '对话互动数字人',
},
component: () => import('@/views/dialogue-detail/dialogue-detail.vue'),
},
{
path: '/dialogue-list-layout',
name: 'DialogueListLayout',
meta: {
rank: 1001,
title: '我的對話',
},
component: Index,
redirect: '/dialogue-list',
children: [
{
path: '/dialogue-list',
name: 'DialogueList',
meta: {
rank: 1001,
title: '我的對話',
},
component: () => import('@/views/dialogue-list/dialogue-list.vue'),
},
],
},
] as RouteRecordRaw[]
import { defineStore } from 'pinia'
import { DigitalHumanDialogueConfig, DigitalHumanDialogueSystemInfo } from '@/store/types/digital-human-dialogue'
function defaultDigitalHumanDialogue(): DigitalHumanDialogueConfig {
return {
baseInfo: {
configId: '',
title: '新建对话',
pageLayout: 'vertical',
isOpen: 'N',
publishStatus: 'N',
},
humanInfo: {
figureId: 'A2A_V2-3to2_leqing',
speed: 5,
intonation: 5,
timebreId: '5132',
},
backgroundInfo: {
backgroundUrl: '',
},
systemInfo: defaultDigitalHumanSystemInfo(),
}
}
function defaultDigitalHumanSystemInfo(): DigitalHumanDialogueSystemInfo {
return {
id: 0,
systemModel: '',
systemDefinition: '',
preamble: '',
perambleStatus: 'Y',
chitchat: '',
chitchatStatus: 'Y',
refuseAnswer: '',
refuseAnswerStatus: 'Y',
}
}
function getLocalState(): DigitalHumanDialogueConfig {
return defaultDigitalHumanDialogue()
}
export const useDigitalHumanDialogueStore = defineStore('digital-human-dialogue-store', {
state: (): DigitalHumanDialogueConfig => getLocalState(),
actions: {
setFigureId(figureId: string) {
this.humanInfo.figureId = figureId
},
setBackgroundImageUrl(backgroundUrl: string) {
this.backgroundInfo.backgroundUrl = backgroundUrl
},
setTimbreId(timbreId: string) {
this.humanInfo.timebreId = timbreId
},
setSpeed(speed: number) {
this.humanInfo.speed = speed
},
setIntonation(intonation: number) {
this.humanInfo.intonation = intonation
},
setSystemInfo(digitalHumanDialogueSystemInfo: DigitalHumanDialogueSystemInfo) {
this.systemInfo = { ...this.systemInfo, ...digitalHumanDialogueSystemInfo }
},
updateDigitalHumanDialogue(digitalCreation: DigitalHumanDialogueConfig) {
this.$state = { ...this.$state, ...digitalCreation }
},
resetDigitalHumanSystemInfo() {
this.systemInfo = defaultDigitalHumanSystemInfo()
},
resetDigitalHumanDialogue() {
this.$state = defaultDigitalHumanDialogue()
},
},
})
export interface DigitalHumanDialogueConfig {
baseInfo: {
configId: string //配置ID
title: string //标题
pageLayout: 'vertical' | 'horizontal' // vertical-竖 horizontal-横
isOpen: 'Y' | 'N' //是否开启 Y-开启 N-未开启
publishStatus: 'Y' | 'N' // 发布状态 Y-已发布 N-未发布
}
humanInfo: {
figureId: string //人像ID
speed: number //速度
intonation: number //音调
timebreId: string //音色ID
}
backgroundInfo: {
backgroundUrl: string //背景图地址
}
systemInfo: DigitalHumanDialogueSystemInfo
}
export interface DigitalHumanDialogueSystemInfo {
id: number
systemModel: string //角色模板
systemDefinition: string //角色定义
preamble: string //开场白
perambleStatus?: 'Y' | 'N' //是否开启开场白 Y-开启 N-未开启
chitchat: string // 闲聊
chitchatStatus?: 'Y' | 'N' //是否开启闲聊 Y-开启 N-未开启
refuseAnswer: string // 拒绝回答
refuseAnswerStatus?: 'Y' | 'N' //是否开启拒绝回答 Y-开启 N-未开启
}
// variable.scss
:root {
--side-bar-content: calc(100vh - 56px - 32px - 16px - 42px - 12px); // 侧边栏内容区域高度
}
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
import 'dayjs/locale/zh-cn'
dayjs.extend(utc)
dayjs.extend(timezone)
export function formatDateTime(date: string | number | Date, format: string = 'YYYY-MM-DD HH:mm:ss'): string {
return dayjs(date).format(format)
}
<script setup lang="ts">
import { computed, ref } from 'vue'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { uploadImageFile } from '@/apis/digital-creation'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
import { fetchGetBackgroundImageList } from '@/apis/digital-human-dialogue'
interface BackgroundImageItem {
id?: number
imageSource: 'PUBLIC' | 'PERSON'
imageName: string
imageUrl: string
}
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
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 searchImageName = ref('')
const uploadLoading = ref(false)
const currentBackgroundImageType = ref('PERSON')
const backgroundImageTypeList = [
{
label: '我的',
value: 'PERSON',
},
{
label: '背景庫',
value: 'PUBLIC',
},
]
const currentBackgroundUrl = computed(() => {
return digitalHumanDialogueStore.backgroundInfo.backgroundUrl
})
getBackgroundImageList()
async function getBackgroundImageList() {
const res = await fetchGetBackgroundImageList<BackgroundImageItem[]>()
if (res.code === 0) {
publicBackgroundImageList.value = res.data.filter((i) => i.imageSource === 'PUBLIC')
personBackgroundImageList.value = res.data.filter((i) => i.imageSource === 'PERSON')
}
}
async function handleSearchImageList() {}
async function uploadImage(event: any) {
const e = window.event || event
const file = e.target.files[0]
const fileName = file.name.substring(0, file.name.lastIndexOf('.'))
const maxImageSize = 1024 * 1024 * 3
if (!['png', 'jpg', 'jpeg'].includes(file.type.split('/')[1])) {
window.$message.error('必须为png或者jpg格式')
return
}
if (file.size > maxImageSize) {
window.$message.error('图片不能超过3MB')
return
}
const URL = window.URL || window.webkitURL
const img = new Image()
img.src = URL.createObjectURL(file)
img.onload = async function () {
const formData = new FormData()
formData.append('file', file)
uploadLoading.value = true
const res = await uploadImageFile(fileName, formData).finally(() => (uploadLoading.value = false))
if (res.code === 0) {
getBackgroundImageList()
}
}
}
function handleClickImage(image: BackgroundImageItem) {
digitalHumanDialogueStore.setBackgroundImageUrl(image.imageUrl)
}
function handleDelete(id: number) {
window.$dialog.warning({
title: '刪除圖片',
content: '是否刪除該圖片?',
positiveText: '確認',
negativeText: '取消',
onPositiveClick: () => {
console.log(id)
window.$message.success('刪除成功')
},
})
}
function onImageLoaded(index: number) {
personBackgroundImageListLoaded.value[index] = true
publicBackgroundImageListLoaded.value[index] = true
}
function handleUpdateBackgroundImageType(backgroundImageType: string) {
currentBackgroundImageType.value = backgroundImageType
}
</script>
<template>
<div class="h-full">
<div class="mb-3 flex items-center gap-4 px-4">
<NButtonGroup>
<NButton
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 }}
</NButton>
</NButtonGroup>
<NInput
v-model:value="searchImageName"
round
placeholder="請輸入名稱"
@blur="handleSearchImageList"
@keyup.enter="handleSearchImageList"
>
<template #prefix>
<CustomIcon class="text-lg" icon="mingcute:search-line" />
</template>
</NInput>
</div>
<NScrollbar v-if="currentBackgroundImageType === 'PERSON'" class="h-[calc(var(--side-bar-content)-46px)]! px-4">
<NGrid :x-gap="12" :y-gap="12" :cols="3">
<NGi>
<NSpin :show="uploadLoading">
<label
class="h-22 w-22 hover:border-blue flex cursor-pointer flex-col items-center justify-center rounded-lg border border-gray-200"
for="upload"
>
<CustomIcon class="text-lg" icon="mingcute:add-line" />
</label>
<input id="upload" type="file" accept="image/*" class="hidden" @change="uploadImage" />
<template #description>上傳中</template>
</NSpin>
</NGi>
<NGi v-for="(image, index) in personBackgroundImageList" :key="index">
<NSpin :show="!publicBackgroundImageListLoaded[index]">
<div
class="h-22 w-22 group relative cursor-pointer overflow-hidden rounded-lg border border-2 bg-gray-100"
:class="image.imageUrl === currentBackgroundUrl ? 'border-blue' : 'border-transparent'"
@click="handleClickImage(image)"
>
<img class="h-full w-full object-contain" :src="image.imageUrl" @load="onImageLoaded(index)" />
<div
class="absolute bottom-0 h-5 w-full bg-gradient-to-t from-gray-600 px-1 text-xs leading-5 text-white"
>
{{ image.imageName }}
</div>
<div
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!)"
>
<CustomIcon icon="mi:delete" class="text-lg text-white" />
</div>
<CustomIcon
v-if="currentBackgroundUrl === image.imageUrl"
icon="si-glyph:checked"
class="text-blue absolute left-0 top-0 text-xs"
/>
</div>
</NSpin>
</NGi>
</NGrid>
</NScrollbar>
<NScrollbar v-if="currentBackgroundImageType === 'PUBLIC'" class="h-[calc(var(--side-bar-content)-46px)]! px-4">
<NGrid :x-gap="12" :y-gap="12" :cols="3">
<NGi v-for="(image, index) in publicBackgroundImageList" :key="index">
<NSpin :show="!publicBackgroundImageListLoaded[index]">
<div
class="h-22 w-22 group relative cursor-pointer overflow-hidden rounded-lg border border-2 bg-gray-100"
:class="currentBackgroundUrl === image.imageUrl ? 'border-blue' : 'border-transparent'"
@click="handleClickImage(image)"
>
<img class="h-full w-full object-contain" :src="image.imageUrl" @load="onImageLoaded(index)" />
<div
class="absolute bottom-0 h-5 w-full bg-gradient-to-t from-gray-600 px-1 text-xs leading-5 text-white"
>
{{ image.imageName }}
</div>
<CustomIcon
v-if="currentBackgroundUrl === image.imageUrl"
icon="si-glyph:checked"
class="text-blue absolute left-0 top-0 text-xs"
/>
</div>
</NSpin>
</NGi>
</NGrid>
</NScrollbar>
</div>
</template>
<script setup lang="ts">
import BackgroundImages from './background-images.vue'
</script>
<template>
<n-tabs type="line" animated>
<n-tab-pane name="images" tab="圖片">
<BackgroundImages />
</n-tab-pane>
</n-tabs>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { FormInst } from 'naive-ui'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { formatDateTime } from '@/utils/date-formatter'
interface Props {
isShowModal: boolean
btnLoading: boolean
modalTitle: string
}
export interface DigitalHumanDialogueForm {
title: string
pageLayout: 'vertical' | 'horizontal'
}
interface Emits {
(e: 'update:isShowModal', value: boolean): void
(e: 'comfirm', value: DigitalHumanDialogueForm): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const digitalHumanDialogueFormRef = ref<FormInst | null>(null)
const digitalHumanDialogueFormData = reactive<DigitalHumanDialogueForm>({
title: '新建對話' + formatDateTime(new Date()),
pageLayout: 'vertical',
})
const digitalHumanDialogueFormRules = {
title: [{ required: true, message: '請輸入交互名稱', trigger: 'blur' }],
}
const pageLayoutList = [
{ label: '豎版', value: 'vertical', icon: 'mynaui:rectangle-vertical' },
{ label: '橫版', value: 'horizontal', icon: 'mynaui:rectangle' },
]
const showModal = computed({
get() {
return props.isShowModal
},
set(value: boolean) {
emit('update:isShowModal', value)
},
})
function handleUpdatePageLayout(pageLayout: 'vertical' | 'horizontal') {
digitalHumanDialogueFormData.pageLayout = pageLayout
}
function handleAddDigitalHumanDialogue() {
digitalHumanDialogueFormRef.value?.validate((errors) => {
if (!errors) {
emit('comfirm', digitalHumanDialogueFormData)
}
})
}
</script>
<template>
<CustomModal v-model:is-show="showModal" :title="modalTitle" :width="448" @confirm="handleAddDigitalHumanDialogue">
<template #content>
<NForm
ref="digitalHumanDialogueFormRef"
:model="digitalHumanDialogueFormData"
:rules="digitalHumanDialogueFormRules"
label-placement="left"
show-require-mark
label-width="80px"
class="pt-2"
>
<NFormItem label="名稱" path="title">
<NInput
v-model:value="digitalHumanDialogueFormData.title"
placeholder="請輸入交互名稱"
:maxlength="30"
show-count
/>
</NFormItem>
<NFormItem label="畫幅比例" path="pageLayout" feedback-style="display:none">
<div class="flex gap-3">
<div
v-for="pageLayoutItem in pageLayoutList"
:key="pageLayoutItem.value"
class="flex h-9 w-[95px] cursor-pointer items-center justify-center gap-2 rounded-md border text-xs"
:class="
pageLayoutItem.value === digitalHumanDialogueFormData.pageLayout
? 'border-theme-color text-theme-color bg-[#e6f0ff]'
: 'border-[#dde3f0]'
"
@click="handleUpdatePageLayout(pageLayoutItem.value as 'vertical' | 'horizontal')"
>
<CustomIcon :icon="pageLayoutItem.icon" class="text-base" />
<span>{{ pageLayoutItem.label }}</span>
</div>
</div>
</NFormItem>
</NForm>
</template>
</CustomModal>
</template>
<style lang="scss" scoped>
:deep(.n-input) {
border-radius: 4px;
.n-input-wrapper {
font-size: 12px;
}
}
</style>
<script setup lang="ts">
import { computed, ref } from 'vue'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { DigitalHumanDialogueTimbreItem } from '../../dialogue-type'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
interface Props {
dialogueTimbreItem?: DigitalHumanDialogueTimbreItem
showToggle?: boolean
}
interface Emits {
(e: 'click', value: string): void
(e: 'toggle', value: boolean): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const digitalAudio = ref<HTMLAudioElement>()
const timebreId = computed(() => digitalHumanDialogueStore.humanInfo.timebreId)
function playAudio() {
digitalAudio.value?.play()
}
</script>
<template>
<div
class="relative mb-4 flex items-center gap-2 rounded-2xl border p-2 hover:shadow"
:class="!showToggle && dialogueTimbreItem?.timebreId === timebreId ? 'border-blue' : 'border-gray-200'"
@click="emit('click', dialogueTimbreItem!.timebreId)"
>
<div
class="h-16 w-16 rounded-lg bg-[url(https://digital-human-js-cdn.cdn.bcebos.com/web_base/20240926112018/digital-human-web-new-base/img/boy.png)] bg-[length:100%_100%]"
></div>
<div class="flex-1 overflow-hidden">
<div class="mb-2 flex items-center gap-2">
<div class="max-w-32 truncate">{{ dialogueTimbreItem?.name }}</div>
<CustomIcon class="cursor-pointer text-lg" icon="mingcute:volume-line" @click.stop.prevent="playAudio" />
</div>
<NScrollbar x-scrollable>
<div class="flex gap-2">
<NTag v-for="(style, index) in dialogueTimbreItem?.style" :key="index" type="warning" round>{{ style }}</NTag>
</div>
</NScrollbar>
</div>
<div v-if="showToggle" class="absolute right-2 top-2">
<CustomIcon class="cursor-pointer text-lg" icon="ant-design:swap-outlined" @click="emit('toggle', true)" />
</div>
</div>
<audio ref="digitalAudio" :src="dialogueTimbreItem?.audioUrl"></audio>
</template>
<script setup lang="ts">
import { fetchDigitalHumanTimbreList, fetchTimbreByExample } from '@/apis/digital-creation'
import { DigitalHumanDialogueTimbreItem } from '../../dialogue-type'
import { computed, onMounted, ref, watch } from 'vue'
import DigitalAudioCard from './digital-audio-card.vue'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const sexValue = ref(0)
const sexList = [
{ key: 0, label: '女性' },
{ key: 1, label: '男性' },
]
const currentSelectedAudioType = ref('mandarin')
const audioTypeList = [
{ label: '語言:普通話', value: 'mandarin', style: { fontSize: '12px' } },
{ label: '語言:粵語', value: 'cantonese', style: { fontSize: '12px' } },
]
const digitalTimbreValue = ref<DigitalHumanDialogueTimbreItem>()
const digitalTimbreList = ref<DigitalHumanDialogueTimbreItem[]>([])
const digitalTimbreFemaleList = ref<DigitalHumanDialogueTimbreItem[]>([])
const digitalTimbreMaleList = ref<DigitalHumanDialogueTimbreItem[]>([])
const showAll = ref(false)
const searchName = ref('')
const speed = computed({
get() {
return digitalHumanDialogueStore.humanInfo.speed
},
set(value) {
digitalHumanDialogueStore.setSpeed(value)
},
})
const intonation = computed({
get() {
return digitalHumanDialogueStore.humanInfo.intonation
},
set(value) {
digitalHumanDialogueStore.setIntonation(value)
},
})
const isMandarinAudioType = computed(() => {
return currentSelectedAudioType.value === 'mandarin'
})
watch(
() => [digitalHumanDialogueStore.humanInfo.timebreId, digitalTimbreList.value.length],
([timebreId, len]) => {
if (timebreId && len) {
digitalTimbreValue.value = digitalTimbreList.value.find((i) => i.timebreId === timebreId)
}
},
)
onMounted(() => {
currentSelectedAudioType.value = 'cantonese'
getDigitalTimbreList()
})
async function getDigitalTimbreList() {
const res = await fetchDigitalHumanTimbreList<DigitalHumanDialogueTimbreItem[]>()
if (res.code === 0) {
digitalTimbreList.value = res.data
digitalTimbreFemaleList.value = digitalTimbreList.value.filter((i) => i.sex === '女')
digitalTimbreMaleList.value = digitalTimbreList.value.filter((i) => i.sex === '男')
}
}
async function handleSearch(value: string) {
const res = await fetchTimbreByExample<DigitalHumanDialogueTimbreItem[]>(value)
if (res.code === 0) {
digitalTimbreList.value = res.data
digitalTimbreFemaleList.value = digitalTimbreList.value.filter((i) => i.sex === '女')
digitalTimbreMaleList.value = digitalTimbreList.value.filter((i) => i.sex === '男')
}
}
function handleClickAudioCard(timbreId: string) {
digitalHumanDialogueStore.setTimbreId(timbreId)
}
</script>
<template>
<NScrollbar v-if="!showAll" class="h-side-bar-content! px-4">
<div class="mb-3 flex justify-end">
<NSelect
v-model:value="currentSelectedAudioType"
class="w-[150px]!"
size="small"
:options="audioTypeList"
placeholder="請選擇語言"
/>
</div>
<DigitalAudioCard
:dialogue-timbre-item="digitalTimbreValue"
:show-toggle="isMandarinAudioType"
@toggle="showAll = true"
/>
<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 text-center">{{ speed }}</div>
</div>
<div class="mt-4 flex items-center gap-2">
<div class="w-12">語調:</div>
<n-slider v-model:value="intonation" class="flex-1" :max="15" :min="0" :step="1" />
<div class="w-10 text-center">{{ intonation }}</div>
</div>
</NScrollbar>
<div v-else>
<div class="flex items-center gap-4 px-4 pb-3">
<n-button text @click="showAll = false">
<template #icon>
<CustomIcon class="text-lg" icon="mingcute:left-line" />
</template>
返回
</n-button>
<n-input v-model:value="searchName" round placeholder="搜索" @input="handleSearch">
<template #prefix>
<CustomIcon class="text-lg" icon="mingcute:search-line" />
</template>
</n-input>
</div>
<div class="flex justify-end px-4 pb-3">
<HorizontalTabs v-model:value="sexValue" :list="sexList" />
</div>
<NScrollbar class="h-[calc(var(--side-bar-content)-92px)]! px-4">
<DigitalAudioCard
v-for="(timbre, index) in sexValue ? digitalTimbreMaleList : digitalTimbreFemaleList"
:key="index"
:dialogue-timbre-item="timbre"
@click="handleClickAudioCard"
/>
</NScrollbar>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { ImageItem } from '@/store/types/creation'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
interface Props {
imageItem: ImageItem
}
interface Emits {
(e: 'click', id: string): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const currentFigureId = computed(() => {
return digitalHumanDialogueStore.humanInfo.figureId
})
</script>
<template>
<div
class="hover:border-theme-color relative h-28 w-16 cursor-pointer overflow-hidden rounded border border-2"
:class="currentFigureId === imageItem.figureId ? 'border-theme-color' : 'border-transparent'"
@click="emit('click', imageItem.figureId as string)"
>
<img v-show="currentFigureId === imageItem.figureId" src="@/assets/svgs/select.svg" class="absolute left-0 top-0" />
<img :src="imageItem.imageUrl" />
<div class="from-gray absolute bottom-0 h-5 w-full bg-gradient-to-t px-1 text-xs leading-5 text-white">
{{ imageItem.imageName }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
import { ImageItem } from '@/store/types/creation'
interface Props {
imageItem: ImageItem
}
interface Emits {
(e: 'click', value: string): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const imageLoading = ref(true)
const figureId = computed(() => {
return digitalHumanDialogueStore.humanInfo.figureId
})
</script>
<template>
<NSpin :show="imageLoading">
<div
class="hover:border-blue relative h-28 w-16 cursor-pointer rounded border border-2 bg-gray-100"
:class="figureId === imageItem.figureId ? 'border-blue' : 'border-transparent'"
@click="emit('click', imageItem.figureId as string)"
>
<img :src="imageItem.imageUrl" class="h-full w-full" @load="imageLoading = false" />
<div class="absolute bottom-0 h-5 w-full bg-gradient-to-t from-gray-600 px-1 text-xs leading-5 text-white">
{{ imageItem.imageName }}
</div>
<CustomIcon
v-if="figureId === imageItem.figureId"
icon="si-glyph:checked"
class="text-blue absolute left-0 top-0 text-xs"
/>
</div>
</NSpin>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import DigitalCard from './digital-human-card.vue'
import {
fetch2DBoutiqueImageList,
fetch2DFewShotImageList,
fetch3DImageList,
fetchInfoByImageName,
} from '@/apis/digital-creation'
import { ImageItem, ImageType } from '@/store/types/creation'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const threeDImageList = ref<ImageItem[]>([])
const twoDBoutiqueImageList = ref<ImageItem[]>([])
const twoDFewShotImageList = ref<ImageItem[]>([])
const allImageList = ref<ImageItem[]>([])
const showAll = ref(false)
const searchImageName = ref('')
onMounted(() => {
getDigitalImageList()
})
async function getDigitalImageList() {
const [res1, res2, res3] = await Promise.all([
fetch3DImageList<ImageItem[]>(),
fetch2DBoutiqueImageList<ImageItem[]>(),
fetch2DFewShotImageList<ImageItem[]>(),
])
res1.code === 0 && (threeDImageList.value = res1.data)
res2.code === 0 && (twoDBoutiqueImageList.value = res2.data)
res3.code === 0 && (twoDFewShotImageList.value = res3.data)
}
async function handleSearchImage() {
const res = await fetchInfoByImageName<ImageItem[]>(searchImageName.value)
if (res.code === 0) {
allImageList.value = res.data
}
}
function handleClickDigitalImage(figureId: string) {
digitalHumanDialogueStore.setFigureId(figureId)
}
function handleClickAll(imageType: ImageType) {
switch (imageType) {
case ImageType.THREE_D:
allImageList.value = threeDImageList.value
break
case ImageType.TWO_D_BOUTIQUE:
allImageList.value = twoDBoutiqueImageList.value
break
case ImageType.TWO_D_FEW_SHOT:
allImageList.value = twoDFewShotImageList.value
break
}
showAll.value = true
}
</script>
<template>
<NScrollbar class="h-side-bar-content! px-4">
<div v-show="!showAll">
<div class="pb-4">
<div class="flex items-center justify-between pb-3 leading-8">
<span>3D數字人</span>
<span class="text-gray cursor-pointer text-xs" @click="handleClickAll(ImageType.THREE_D)">全部</span>
</div>
<div class="flex flex-wrap gap-3">
<DigitalCard
v-for="item in threeDImageList.slice(0, 4)"
:key="item.id"
:image-item="item"
@click="handleClickDigitalImage"
/>
</div>
</div>
<div class="pb-4">
<div class="flex items-center justify-between pb-3 leading-8">
<span>2D精品數字人</span>
<span class="text-gray cursor-pointer text-xs" @click="handleClickAll(ImageType.TWO_D_BOUTIQUE)">全部</span>
</div>
<div class="flex flex-wrap gap-3">
<DigitalCard
v-for="item in twoDBoutiqueImageList.slice(0, 4)"
:key="item.id"
:image-item="item"
@click="handleClickDigitalImage"
/>
</div>
</div>
<div class="pb-4">
<div class="flex items-center justify-between pb-3 leading-8">
<span>2D小樣本數字人</span>
<span class="text-gray cursor-pointer text-xs" @click="handleClickAll(ImageType.TWO_D_FEW_SHOT)">全部</span>
</div>
<div class="flex flex-wrap gap-3">
<DigitalCard
v-for="item in twoDFewShotImageList.slice(0, 4)"
:key="item.id"
:image-item="item"
@click="handleClickDigitalImage"
/>
</div>
</div>
</div>
<div v-show="showAll">
<div class="flex items-center gap-4 pb-3">
<NButton text @click="showAll = false">
<template #icon>
<CustomIcon class="text-lg" icon="mingcute:left-line" />
</template>
返回
</NButton>
<NInput
v-model:value="searchImageName"
round
placeholder="輸入名稱搜索"
@blur="handleSearchImage"
@keyup.enter="handleSearchImage"
>
<template #prefix>
<CustomIcon class="text-lg" icon="mingcute:search-line" />
</template>
</NInput>
</div>
<div class="flex flex-wrap gap-3">
<DigitalCard v-for="item in allImageList" :key="item.id" :image-item="item" @click="handleClickDigitalImage" />
</div>
</div>
</NScrollbar>
</template>
<script setup lang="ts">
import DigitalAudio from './digital-audio.vue'
import DigitalHuman from './digital-human.vue'
</script>
<template>
<n-tabs type="line" animated>
<n-tab-pane name="human" tab="選擇">
<DigitalHuman />
</n-tab-pane>
<n-tab-pane name="audio" tab="聲音">
<DigitalAudio />
</n-tab-pane>
</n-tabs>
</template>
<style lang="scss" scoped>
:deep(.n-base-selection) {
font-size: 12px;
border-radius: 4px;
}
</style>
<script setup lang="ts">
import { computed } from 'vue'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const backgroundImageUrl = computed(() => {
return digitalHumanDialogueStore.backgroundInfo.backgroundUrl
})
</script>
<template>
<div class="h-[calc(100%-190px)] w-[calc(100%-32px)] overflow-hidden rounded-2xl bg-white">
<img v-show="backgroundImageUrl" :src="backgroundImageUrl" alt="背景圖" class="h-full w-full object-cover" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { AI_INDEX_URLS } from '@/config/base-url'
interface Props {
isShowModal: boolean
btnLoading: boolean
modalTitle: string
configId: string
}
interface Emits {
(e: 'update:isShowModal', value: boolean): void
(e: 'comfirm'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const shareLink = computed(() => {
return `${AI_INDEX_URLS[window.ENV || 'DEV']}test/${props.configId}`
})
const showModal = computed({
get() {
return props.isShowModal
},
set(value: boolean) {
emit('update:isShowModal', value)
},
})
function handleCopyLink() {
window.$message.success('複製成功')
}
</script>
<template>
<CustomModal v-model:is-show="showModal" :title="modalTitle" :width="448" :height="180" @confirm="emit('comfirm')">
<template #content>
<p class="text-[#4b4b4b]">可複製鏈接,在網頁端預覽</p>
<NInputGroup class="mt-4">
<NInput :value="shareLink" size="large" disabled class="w-[360px]!" />
<NButton type="info" size="large" class="rounded-r-md! text-sm!" @click="handleCopyLink">複製鏈接</NButton>
</NInputGroup>
</template>
<template #footer> </template>
</CustomModal>
</template>
<style lang="scss" scoped>
:deep(.n-input) {
border-radius: 6px;
.n-input-wrapper {
font-size: 12px;
}
}
</style>
<script setup lang="ts">
import { computed } from 'vue'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const digitalHumanDialogueSystemInfo = computed(() => {
return digitalHumanDialogueStore.systemInfo
})
function handleUpdateSystemInfo() {
window.$dialog.warning({
title: '更換模板',
content: '更換模版將清空當前⻆色的所有設定,是否確認更換?',
negativeText: '取 消',
positiveText: '確 認',
onPositiveClick: () => {
digitalHumanDialogueStore.resetDigitalHumanSystemInfo()
},
})
}
</script>
<template>
<NScrollbar class="h-side-bar-content! px-4">
<NForm>
<NFormItem label="角色模板">
<div class="flex h-8 w-full items-center justify-between rounded border px-3 text-xs">
<span>{{ digitalHumanDialogueSystemInfo.systemModel }}</span>
<span class="text-theme-color cursor-pointer hover:opacity-80" @click="handleUpdateSystemInfo">更換</span>
</div>
</NFormItem>
<NFormItem>
<template #label>
<div class="flex items-center gap-1">
<span>角色定義</span>
<NTooltip trigger="hover">
<template #trigger>
<CustomIcon
icon="material-symbols:help-outline"
class="cursor-pointer text-base text-[#999] outline-none"
/>
</template>
可以定義⻆色的名字、職業、語言⻛格、回答方式等
</NTooltip>
</div>
</template>
<NInput
v-model:value="digitalHumanDialogueSystemInfo.systemDefinition"
type="textarea"
placeholder="請輸入對角色的基本描述"
:autosize="{ minRows: 4 }"
maxlength="500"
show-count
/>
</NFormItem>
</NForm>
</NScrollbar>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import RoleDefine from './role-define.vue'
import RoleTechnique from './role-technique.vue'
import RoleSpeech from './role-speech.vue'
import RoleSystemList from './role-system-list.vue'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
import { DigitalHumanDialogueSystemInfo } from '@/store/types/digital-human-dialogue'
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const isShowSelectedDialogueSystemList = computed(() => {
return !digitalHumanDialogueStore.systemInfo.id && !digitalHumanDialogueStore.systemInfo.systemModel
})
function handleSelectedDialogueSystem(dialogueSystem: DigitalHumanDialogueSystemInfo) {
digitalHumanDialogueStore.setSystemInfo(dialogueSystem)
}
</script>
<template>
<div v-show="!isShowSelectedDialogueSystemList">
<n-tabs type="line">
<n-tab-pane name="define" tab="定義">
<RoleDefine />
</n-tab-pane>
<n-tab-pane name="technique" tab="技能">
<RoleTechnique />
</n-tab-pane>
<n-tab-pane name="speech" tab="話術">
<RoleSpeech />
</n-tab-pane>
</n-tabs>
</div>
<div v-show="isShowSelectedDialogueSystemList" class="py-2">
<p class="mb-3 px-4">請選擇角色列表</p>
<RoleSystemList @click="handleSelectedDialogueSystem" />
</div>
</template>
<style lang="scss" scoped>
:deep(.n-input.n-input--textarea) {
font-size: 12px;
border-radius: 4px;
}
</style>
<script setup lang="ts">
import { computed } from 'vue'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const digitalHumanDialogueSystemInfo = computed(() => {
return digitalHumanDialogueStore.systemInfo
})
function handleUpdatePerambleStatus(isOpenPeramble: boolean) {
digitalHumanDialogueSystemInfo.value.perambleStatus = isOpenPeramble ? 'Y' : 'N'
}
</script>
<template>
<NScrollbar class="h-side-bar-content! px-4">
<NForm>
<NFormItem>
<template #label>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
<span>開場話術</span>
<NTooltip trigger="hover">
<template #trigger>
<CustomIcon
icon="material-symbols:help-outline"
class="cursor-pointer text-base text-[#999] outline-none"
/>
</template>
高級模式下,請在客悦智能對話平台配置開場話術
</NTooltip>
</div>
<NSwitch
:value="digitalHumanDialogueSystemInfo.perambleStatus === 'Y'"
@update:value="handleUpdatePerambleStatus"
/>
</div>
</template>
<NInput
v-model:value="digitalHumanDialogueSystemInfo.preamble"
type="textarea"
placeholder="請描述角色的開場話術"
:autosize="{ minRows: 4 }"
maxlength="500"
show-count
/>
</NFormItem>
<!-- <NFormItem>
<template #label>
<div class="flex w-[292px] items-center justify-between">
<div class="flex items-center gap-1">
<span>安撫話術</span>
<NTooltip trigger="hover">
<template #trigger>
<CustomIcon
icon="material-symbols:help-outline"
class="cursor-pointer text-base text-[#999] outline-none"
/>
</template>
等待生成回覆時自動播報安撫用户的話術,每次等待回覆時隨機播報其中一條,最多可以添加20條
</NTooltip>
</div>
<NSwitch />
</div>
</template>
<NDynamicInput class="aaa" placeholder="請輸入安撫用户的話術" :min="0" :max="20">
<template #create-button-default>
<span class="text-xs">添加話術</span>
</template>
</NDynamicInput>
</NFormItem> -->
</NForm>
</NScrollbar>
</template>
<style lang="scss" scoped>
:deep(.n-form-item-label__text) {
width: 100%;
}
</style>
<script setup lang="ts">
import { DigitalHumanDialogueSystemInfo } from '@/store/types/digital-human-dialogue'
interface Props {
roleSystemItem: DigitalHumanDialogueSystemInfo
}
defineProps<Props>()
const emit = defineEmits<{
(e: 'click', roleSystemItem: DigitalHumanDialogueSystemInfo): void
}>()
function handleSelectedDialogueSystem(roleSystemItem: DigitalHumanDialogueSystemInfo) {
emit('click', roleSystemItem)
}
</script>
<template>
<div
class="flex h-[70px] w-full cursor-pointer items-center rounded-lg border border-gray-200 px-6 hover:shadow"
@click="handleSelectedDialogueSystem(roleSystemItem)"
>
{{ roleSystemItem.systemModel }}
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { fetchDigitalHumanDialogueSystemList } from '@/apis/digital-human-dialogue'
import RoleSystemItem from './role-system-item.vue'
import { DigitalHumanDialogueSystemInfo } from '@/store/types/digital-human-dialogue'
const emit = defineEmits<{
(e: 'click', roleSystemItem: DigitalHumanDialogueSystemInfo): void
}>()
const digitalHumanDialogueSystemList = ref<DigitalHumanDialogueSystemInfo[]>([])
onMounted(() => {
handleGetDigitalHumanDialogueSystemList()
})
async function handleGetDigitalHumanDialogueSystemList() {
const res = await fetchDigitalHumanDialogueSystemList<DigitalHumanDialogueSystemInfo[]>({
pagingInfo: { pageNo: 1, pageSize: 999 },
})
if (res.code === 0) {
digitalHumanDialogueSystemList.value = res.data
}
}
</script>
<template>
<NScrollbar class="h-[calc(var(--side-bar-content))]! px-4">
<div class="flex flex-col gap-3">
<RoleSystemItem
v-for="digitalHumanDialogueSystemItem in digitalHumanDialogueSystemList"
:key="digitalHumanDialogueSystemItem.id"
:role-system-item="digitalHumanDialogueSystemItem"
@click="emit('click', digitalHumanDialogueSystemItem)"
/>
</div>
</NScrollbar>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const digitalHumanDialogueSystemInfo = computed(() => {
return digitalHumanDialogueStore.systemInfo
})
function handleUpdateChitChatStatus(isOpenChitChat: boolean) {
digitalHumanDialogueSystemInfo.value.chitchatStatus = isOpenChitChat ? 'Y' : 'N'
}
function handleUpdateRefuseAnswerStatus(isOpenRefuseAnswer: boolean) {
digitalHumanDialogueSystemInfo.value.refuseAnswerStatus = isOpenRefuseAnswer ? 'Y' : 'N'
}
</script>
<template>
<NScrollbar class="h-side-bar-content! px-4">
<NForm>
<NFormItem>
<template #label>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
<span>閒聊</span>
<NTooltip trigger="hover">
<template #trigger>
<CustomIcon
icon="material-symbols:help-outline"
class="cursor-pointer text-base text-[#999] outline-none"
/>
</template>
請描述進行閒聊的問題範圍,如:個人相關話題或常識問題
</NTooltip>
</div>
<NSwitch
:value="digitalHumanDialogueSystemInfo.chitchatStatus === 'Y'"
@update:value="handleUpdateChitChatStatus"
/>
</div>
</template>
<NInput
v-model:value="digitalHumanDialogueSystemInfo.chitchat"
type="textarea"
placeholder="請描述角色進行閒聊的範圍"
:autosize="{ minRows: 4 }"
maxlength="500"
show-count
/>
</NFormItem>
<NFormItem>
<template #label>
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
<span>拒絕回答</span>
<NTooltip trigger="hover">
<template #trigger>
<CustomIcon
icon="material-symbols:help-outline"
class="cursor-pointer text-base text-[#999] outline-none"
/>
</template>
請描述需要拒絕回答的問題範圍,如:非法內容、品牌的負面消息
</NTooltip>
</div>
<NSwitch
:value="digitalHumanDialogueSystemInfo.refuseAnswerStatus === 'Y'"
@update:value="handleUpdateRefuseAnswerStatus"
/>
</div>
</template>
<NInput
v-model:value="digitalHumanDialogueSystemInfo.refuseAnswer"
type="textarea"
placeholder="請描述角色拒絕回答的範圍"
:autosize="{ minRows: 4 }"
maxlength="500"
show-count
/>
</NFormItem>
</NForm>
</NScrollbar>
</template>
<style lang="scss" scoped>
:deep(.n-form-item-label__text) {
width: 100%;
}
</style>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
import { fetchGetInfoByFigureId } from '@/apis/digital-human-dialogue'
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const figureImageUrl = ref('')
const figureId = computed(() => {
return digitalHumanDialogueStore.humanInfo.figureId
})
watch(
() => figureId.value,
(newValue) => {
newValue && handleGetInfoByFigureId(newValue)
},
{ immediate: true },
)
const backgroundImageUrl = computed(() => {
return digitalHumanDialogueStore.backgroundInfo.backgroundUrl
})
async function handleGetInfoByFigureId(figureId: string) {
const res = await fetchGetInfoByFigureId<{ imageUrl: string }>(figureId)
if (res.code === 0) {
figureImageUrl.value = res.data.imageUrl
}
}
</script>
<template>
<div class="relative h-[667px] w-[375px] overflow-hidden rounded-2xl bg-white">
<img
v-show="backgroundImageUrl"
:src="backgroundImageUrl"
alt="背景圖"
class="absolute left-0 top-0 h-full w-full object-cover"
/>
<img
v-show="figureImageUrl"
:src="figureImageUrl"
alt="數字人"
class="absolute left-0 top-0 h-full w-full object-cover"
/>
</div>
</template>
<script setup lang="ts">
import Layout from './layout/index.vue'
</script>
<template>
<Layout />
</template>
export interface DigitalHumanDialogueTimbreItem {
id: number
name: string
timebreId: string
sex: string
style: string[]
applyScene: string[]
audioUrl: string
iconUrl: string
isAudioPlay: boolean
}
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import CustomIcon from '@/components/custom-icon/custom-icon.vue'
import CreateDigitalHumanDialogueModal from '../components/create-digital-human-dialogue-modal.vue'
import PublishDigitalHumanDialogueModal from '../components/publish-digital-human-dialogue-modal.vue'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
const router = useRouter()
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const isEditDigitalHumanDialogueTitle = ref(false)
const digitalHumanDialogueTitle = ref(digitalHumanDialogueStore.baseInfo.title)
const isShowPublishDialogueModal = ref(false)
const showExportModal = ref(false)
function handleBackHome() {
router.replace({ name: 'Home' })
}
function handleShowEditDigitalHumanDialogueTitle() {
isEditDigitalHumanDialogueTitle.value = true
}
function handleSaveDigitalHumanDialogueTitle() {
isEditDigitalHumanDialogueTitle.value = false
if (!digitalHumanDialogueTitle.value) {
window.$message.error('名稱不能為空')
digitalHumanDialogueTitle.value = digitalHumanDialogueStore.baseInfo.title
return
}
window.$message.success('保存成功')
}
function handleSaveDigitalHumanDialogueConfig() {
window.$message.success('保存成功')
}
function handlePublishDigitalHumanDialogue() {
isShowPublishDialogueModal.value = true
window.$message.success('發佈成功')
}
</script>
<template>
<header class="flex h-14 items-center justify-between bg-white px-4">
<div class="flex items-center">
<CustomIcon class="mr-4 cursor-pointer text-lg" icon="mingcute:left-line" @click="handleBackHome" />
<div v-show="!isEditDigitalHumanDialogueTitle" class="flex items-center">
<span class="font-500">{{ digitalHumanDialogueTitle }}</span>
<CustomIcon
icon="bxs:edit"
class="text-theme-color ml-1 cursor-pointer text-lg"
@click="handleShowEditDigitalHumanDialogueTitle"
/>
</div>
<div v-show="isEditDigitalHumanDialogueTitle">
<NInput
v-model:value="digitalHumanDialogueTitle"
placeholder="請輸入名稱"
show-count
clearable
:maxlength="30"
class="w-[300px]!"
@blur="handleSaveDigitalHumanDialogueTitle"
/>
</div>
</div>
<div class="flex items-center">
<div class="flex items-center gap-4">
<NButton class="w-[72px]! h-[32px]! rounded-md!" @click="handleSaveDigitalHumanDialogueConfig"> 保存 </NButton>
<NButton class="w-[72px]! h-[32px]! rounded-md!" type="info" @click="handlePublishDigitalHumanDialogue">
發佈
</NButton>
</div>
</div>
</header>
<CreateDigitalHumanDialogueModal
v-model:is-show-modal="showExportModal"
modal-title="新建對話"
:btn-loading="false"
/>
<PublishDigitalHumanDialogueModal
v-model:is-show-modal="isShowPublishDialogueModal"
modal-title="發佈詳情"
:btn-loading="false"
:config-id="digitalHumanDialogueStore.baseInfo.configId"
/>
</template>
<style lang="scss" scoped>
:deep(.n-input) {
border-radius: 6px;
.n-input-wrapper {
font-size: 12px;
}
}
</style>
<script setup lang="ts">
import { onMounted } from 'vue'
import HeaderBar from './header-bar.vue'
import MainContent from './main-content.vue'
import SideBar from './side-bar.vue'
onMounted(() => {})
</script>
<template>
<div class="h-screen bg-[#f3f4fb]">
<n-layout content-class="layout-wrapper-content" class="h-full !bg-transparent">
<n-layout-header class="!bg-transparent">
<HeaderBar />
</n-layout-header>
<n-layout has-sider class="flex-1 !bg-transparent p-4">
<n-layout-content class="rounded-2xl !bg-transparent">
<MainContent />
</n-layout-content>
<n-layout-sider class="!bg-transparent" width="420">
<SideBar />
</n-layout-sider>
</n-layout>
</n-layout>
</div>
</template>
<style lang="scss" scoped>
:deep(.layout-wrapper-content) {
@apply flex flex-col;
}
</style>
<script setup lang="ts">
import { computed } from 'vue'
import VerticalScreenPreviewContent from '../components/vertical-screen-preview-content.vue'
import HorizontalScreenPreviewContent from '../components/horizontal-screen-preview-content.vue'
import { useDigitalHumanDialogueStore } from '@/store/modules/digital-human-dialogue'
const digitalHumanDialogueStore = useDigitalHumanDialogueStore()
const isShowVerticalScreen = computed(() => {
return digitalHumanDialogueStore.baseInfo.pageLayout === 'vertical'
})
const isShowHorizontalScreen = computed(() => {
return digitalHumanDialogueStore.baseInfo.pageLayout === 'horizontal'
})
</script>
<template>
<main class="flex h-full flex-col items-center justify-center gap-4">
<VerticalScreenPreviewContent v-if="isShowVerticalScreen" />
<HorizontalScreenPreviewContent v-if="isShowHorizontalScreen" />
</main>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import BackgroundSetting from '../components/background/background-setting.vue'
import RoleSetting from '../components/role/role-setting.vue'
import DigitalSetting from '../components/digital/digital-setting.vue'
const value = ref('digital')
const barList = [
{
key: 'digital',
label: '數字人',
icon: 'icon-park-outline:robot',
},
{
key: 'role',
label: '角色',
icon: 'ri:id-card-line',
},
{
key: 'background',
label: '背景',
icon: 'icon-park-outline:background-color',
},
]
</script>
<template>
<section class="h-full pl-4">
<div class="flex h-full rounded-2xl bg-white">
<div class="flex-1 overflow-hidden py-2">
<DigitalSetting v-if="value === 'digital'" />
<RoleSetting v-if="value === 'role'" />
<BackgroundSetting v-if="value === 'background'" />
</div>
<VerticalTabs v-model:value="value" class="border-l" :list="barList"></VerticalTabs>
</div>
</section>
</template>
<style lang="scss" scoped>
:deep(.n-tabs-nav-scroll-wrapper) {
padding: 0 16px;
}
</style>
import { formatDateTime } from '@/utils/date-formatter'
import { NSwitch } from 'naive-ui'
import { h } from 'vue'
export interface DiaglogueTableItem {
configId: string
title: string
modifiedTime: string
modifiedName: string
isOpen: 'Y' | 'N'
publishStatus: 'Y' | 'N'
}
export function createDiaglogueTableColumn(
handleDiaglogueAction: (actionType: string, configId: string, diaglogueTableItem?: DiaglogueTableItem) => void,
) {
return [
{
type: 'selection',
fixed: 'left',
key: 'configId',
render(row: DiaglogueTableItem) {
return row.configId
},
},
{
title: '交互名稱',
key: 'title',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 240,
fixed: 'left',
render(row: DiaglogueTableItem) {
return h('div', { class: 'text-sm' }, row.title || '--')
},
},
{
title: '編輯時間',
key: 'modifiedTime',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 170,
render(row: DiaglogueTableItem) {
return row.modifiedTime ? formatDateTime(row.modifiedTime) : '--'
},
},
{
title: '編輯人',
key: 'modifiedName',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 210,
render(row: DiaglogueTableItem) {
return row.modifiedName || '--'
},
},
{
title: '狀態',
key: 'isOpen',
align: 'left',
width: 130,
render(row: DiaglogueTableItem) {
return h(
NSwitch,
{
value: row.isOpen === 'Y',
onUpdateValue: () => handleDiaglogueAction('updateOpen', row.configId, row),
},
{},
)
},
},
{
title: '操作',
key: 'action',
align: 'left',
ellipsis: {
tooltip: true,
},
width: 250,
fixed: 'right',
render(row: DiaglogueTableItem) {
return [
h(
'span',
{
style: { marginRight: '20px' },
className: 'text-theme-color cursor-pointer hover:opacity-80',
onClick: () => handleDiaglogueAction('edit', row.configId),
},
{ default: () => '編輯' },
),
h(
'span',
{
style: { marginRight: '20px' },
className: 'text-theme-color cursor-pointer hover:opacity-80',
onClick: () => handleDiaglogueAction('copy', row.configId),
},
{ default: () => '複製' },
),
h(
'span',
{
style: { marginRight: '20px' },
className: 'text-theme-color cursor-pointer hover:opacity-80',
onClick: () => handleDiaglogueAction('delete', row.configId),
},
{ default: () => '刪除' },
),
row.publishStatus === 'Y' &&
h(
'span',
{
style: { marginRight: '20px' },
className: 'text-theme-color cursor-pointer hover:opacity-80',
onClick: () => handleDiaglogueAction('publishDetail', row.configId),
},
{ default: () => '發佈詳情' },
),
]
},
},
]
}
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { FormInst } from 'naive-ui'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
interface Props {
isShowModal: boolean
configTitle: string
btnLoading: boolean
modalTitle: string
}
interface Emits {
(e: 'update:isShowModal', value: boolean): void
(e: 'comfirm', value: string): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const digitalHumanDialogueFormRef = ref<FormInst | null>(null)
const digitalHumanDialogueFormData = reactive({
title: '',
})
const digitalHumanDialogueFormRules = {
title: [{ required: true, message: '請輸入交互名稱', trigger: 'blur' }],
}
const showModal = computed({
get() {
return props.isShowModal
},
set(value: boolean) {
emit('update:isShowModal', value)
},
})
watch(
() => props.configTitle,
(newValue) => {
digitalHumanDialogueFormData.title = newValue + '副本'
},
)
function handleAddDigitalHumanDialogue() {
digitalHumanDialogueFormRef.value?.validate((errors) => {
if (!errors) {
emit('comfirm', digitalHumanDialogueFormData.title)
}
})
}
</script>
<template>
<CustomModal
v-model:is-show="showModal"
:title="modalTitle"
:width="448"
:height="220"
@confirm="handleAddDigitalHumanDialogue"
>
<template #content>
<NForm
ref="digitalHumanDialogueFormRef"
:model="digitalHumanDialogueFormData"
:rules="digitalHumanDialogueFormRules"
label-placement="left"
show-require-mark
label-width="80px"
class="pt-2"
>
<NFormItem label="交互名稱" path="title">
<NInput
v-model:value="digitalHumanDialogueFormData.title"
placeholder="請輸入交互名稱"
:maxlength="30"
show-count
/>
</NFormItem>
</NForm>
</template>
</CustomModal>
</template>
<style lang="scss" scoped>
:deep(.n-input) {
border-radius: 4px;
.n-input-wrapper {
font-size: 12px;
}
}
</style>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Search, CloseOne } from '@icon-park/vue-next'
import { createDiaglogueTableColumn, DiaglogueTableItem } from './columns'
import EditDigitalHumanDialogueModal from './components/edit-digital-human-dialogue-modal.vue'
import CustomPagination, { PaginationInfo } from '@/components/custom-pagination/custom-pagination.vue'
import PublishDigitalHumanDialogueModal from '@/views/dialogue-detail/components/publish-digital-human-dialogue-modal.vue'
import {
fetchDelectDigitalHumanDialogueConfigByConfigId,
fetchGetDigitalHumanDialogueConfigByConfigId,
fetchGetDigitalHumanDialogueList,
fetchMultiDelectDigitalHumanDialogueConfig,
fetchSaveDigitalHumanDialogueConfig,
fetchUpdateDigitalHumanDialogueOpen,
} from '@/apis/digital-human-dialogue'
import { DigitalHumanDialogueConfig } from '@/store/types/digital-human-dialogue'
import useTableScrollY from '@/composables/useTableScrollY'
const router = useRouter()
const { pageContentWrapRef, tableContentY } = useTableScrollY(48 + 28 + 24 + 44 + 48 + 48)
const checkedConfigIdList = ref<string[]>([])
const digitalHumanDialogueList = ref<DiaglogueTableItem[]>([])
const dialogueTableColumns = createDiaglogueTableColumn(handleDiaglogueAction)
const searchQuery = ref('')
const dialogueTableLoading = ref(false)
const copyDiaglogueConfig = ref<DigitalHumanDialogueConfig>()
const isShowEditDigitalHumanDialogueModal = ref(false)
const editDigitalHumanDialogueBtnLoading = ref(false)
const pagingInfo = ref<PaginationInfo>({
pageNo: 1,
pageSize: 10,
totalPages: 0,
totalRows: 0,
})
const isShowPublishDigitalHumanDialogueModal = ref(false)
const publishDetailConfigId = ref('')
const emtpyDialogueText = computed(() => {
return searchQuery.value ? '暫⽆搜索結果' : '還沒有任何對話'
})
const isShowPagination = computed(() => {
return tableContentY.value > 0
})
onMounted(() => {
handleGetDiaglogList()
})
async function handleGetDiaglogList() {
dialogueTableLoading.value = true
const res = await fetchGetDigitalHumanDialogueList<DiaglogueTableItem[]>(searchQuery.value)
if (res.code === 0) {
digitalHumanDialogueList.value = res.data
dialogueTableLoading.value = false
}
}
async function handleMultiDeleteDiaglogueConfig() {
window.$dialog.warning({
title: '確認刪除該交互項目?',
content: '刪除後不可恢復,基於該交互項目發佈的應用將停止響應。',
negativeText: '取消',
positiveText: '確認',
onPositiveClick: async () => {
const res = await fetchMultiDelectDigitalHumanDialogueConfig(checkedConfigIdList.value)
if (res.code === 0) {
window.$message.success('刪除成功')
checkedConfigIdList.value = []
await handleGetDiaglogList()
}
},
})
}
function handleClearSearchQuery() {
searchQuery.value = ''
handleGetDiaglogList()
}
function handleSearchDiaglogList() {
handleGetDiaglogList()
}
function handleUpdateCheckedDialogueList(configIdList: string[]) {
checkedConfigIdList.value = configIdList
}
function handleDiaglogueAction(actionType: string, configId: string, diaglogueTableItem?: DiaglogueTableItem) {
switch (actionType) {
case 'updateOpen':
handleUpdateDialogueOpen(configId, diaglogueTableItem!)
break
case 'edit':
handleEditDiaglogueConfig(configId)
break
case 'copy':
handleCopyDiaglogueConfig(configId)
break
case 'delete':
handleDeleteDiaglogueConfig(configId)
break
case 'publishDetail':
handleShowPubishDetailModal(configId)
break
}
}
async function handleUpdateDialogueOpen(configId: string, diaglogueTableItem: DiaglogueTableItem) {
if (diaglogueTableItem.isOpen === 'Y') {
window.$dialog.warning({
title: '停用交互項目',
content: '停⽤後發佈的應⽤將停⽌響應,如有需要可再次啓⽤,確認停⽤該交互項⽬?',
negativeText: '取消',
positiveText: '確認',
onPositiveClick: async () => {
const res = await fetchUpdateDigitalHumanDialogueOpen(configId, 'N')
if (res.code === 0) {
diaglogueTableItem.isOpen = 'N'
await handleGetDiaglogList()
}
},
})
return
}
const res = await fetchUpdateDigitalHumanDialogueOpen(configId, 'Y')
if (res.code === 0) {
diaglogueTableItem.isOpen = 'Y'
await handleGetDiaglogList()
}
}
function handleEditDiaglogueConfig(configId: string) {
router.push({
name: 'DialogueDetail',
params: {
configId,
},
})
}
async function handleCopyDiaglogueConfig(agentId: string) {
const res = await fetchGetDigitalHumanDialogueConfigByConfigId<DigitalHumanDialogueConfig>(agentId)
if (res.code === 0) {
const payload = res.data
copyDiaglogueConfig.value = payload
isShowEditDigitalHumanDialogueModal.value = true
}
}
async function handleAddDigitalHumanDialogue(title: string) {
editDigitalHumanDialogueBtnLoading.value = true
copyDiaglogueConfig.value!.baseInfo.configId = ''
copyDiaglogueConfig.value!.baseInfo.title = title
const res = await fetchSaveDigitalHumanDialogueConfig(copyDiaglogueConfig.value!).finally(
() => (editDigitalHumanDialogueBtnLoading.value = false),
)
if (res.code === 0) {
isShowEditDigitalHumanDialogueModal.value = false
await handleGetDiaglogList()
}
}
function handleDeleteDiaglogueConfig(configId: string) {
window.$dialog.warning({
title: '確認刪除該交互項目?',
content: '刪除後不可恢復,基於該交互項目發佈的應用將停止響應。',
negativeText: '取消',
positiveText: '確認',
onPositiveClick: async () => {
const res = await fetchDelectDigitalHumanDialogueConfigByConfigId(configId)
if (res.code === 0) {
window.$message.success('刪除成功')
// agentAppList.value.length === 1 && (pagingInfo.value.pageNo = pagingInfo.value.pageNo - 1)
await handleGetDiaglogList()
}
},
})
}
function handleShowPubishDetailModal(configId: string) {
isShowPublishDigitalHumanDialogueModal.value = true
publishDetailConfigId.value = configId
}
</script>
<template>
<div ref="pageContentWrapRef" class="h-full w-full overflow-auto rounded-[16px] bg-white p-6">
<p class="mb-6 select-none text-lg text-[#333]">我的對話</p>
<div class="flex justify-between">
<div>
<NButton
class="!rounded-[4px]"
:class="{ '!border-[#2468f2] !bg-[#2468f2] !text-white': checkedConfigIdList.length > 0 }"
:disabled="!checkedConfigIdList.length"
@click="handleMultiDeleteDiaglogueConfig"
>
刪除
</NButton>
<span v-show="checkedConfigIdList.length" class="color-[#999] ml-[8px] text-[12px]">
選擇{{ checkedConfigIdList.length }}條記錄
</span>
</div>
<div
class="border-1 mb-[10px] flex h-[34px] w-[226px] items-center justify-center rounded-[6px] border-[#000]/[0.25] pr-[11px] text-[12px]"
>
<input
v-model="searchQuery"
type="text"
class="mx-[11px] w-[183px] border-none outline-none"
placeholder="請輸入名稱進行蒐索"
@keyup.enter="handleSearchDiaglogList"
/>
<CloseOne
v-show="searchQuery"
theme="filled"
size="16"
fill="#D4D6D9"
class="mr-[5px] cursor-pointer"
@click="handleClearSearchQuery"
/>
<Search theme="outline" size="16" fill="#00000040" class="cursor-pointer" @click="handleSearchDiaglogList" />
</div>
</div>
<NDataTable
:loading="dialogueTableLoading"
:bordered="false"
:single-line="false"
:columns="dialogueTableColumns"
:data="digitalHumanDialogueList"
:row-key="(row: DiaglogueTableItem) => row.configId"
:scroll-x="1056"
:max-height="tableContentY"
@update:checked-row-keys="handleUpdateCheckedDialogueList"
>
<template #empty>
<div :style="{ height: tableContentY + 'px' }" class="flex items-center justify-center">
<div v-show="!dialogueTableLoading" class="flex flex-col items-center justify-center">
<div class="h-[123px] w-[156px] bg-[url('@/assets/images/empty-dialogue.png')] bg-[length:100%_100%]" />
<span class="mt-3 text-[#5b647a]">{{ emtpyDialogueText }}</span>
</div>
</div>
</template>
</NDataTable>
<div v-show="isShowPagination" class="mt-5 flex w-full justify-end">
<CustomPagination :paging-info="pagingInfo" />
</div>
</div>
<EditDigitalHumanDialogueModal
v-model:is-show-modal="isShowEditDigitalHumanDialogueModal"
:config-title="copyDiaglogueConfig?.baseInfo.title || ''"
:btn-loading="editDigitalHumanDialogueBtnLoading"
modal-title="編輯新交互名稱"
@comfirm="handleAddDigitalHumanDialogue"
/>
<PublishDigitalHumanDialogueModal
v-model:is-show-modal="isShowPublishDigitalHumanDialogueModal"
modal-title="發佈詳情"
:btn-loading="false"
:config-id="publishDetailConfigId"
/>
</template>
<script setup lang="ts"></script>
<template>
<main>
<main class="h-full w-full">
<RouterView />
</main>
</template>
......@@ -28,6 +28,18 @@ const menuOptions = shallowReadonly<MenuOption[]>([
},
],
},
{
type: 'group',
label: '對話',
key: 'dialogue',
children: [
{
label: '我的對話',
key: 'dialogueList',
onClick: () => router.push({ name: 'DialogueList' }),
},
],
},
])
</script>
......
declare interface Window {
ENV: 'DEV' | 'PROD'
$loadingBar: import('naive-ui').LoadingBarProviderInst
$dialog: import('naive-ui').DialogProviderInst
$message: import('naive-ui').MessageProviderInst
......
......@@ -30,5 +30,11 @@ export default defineConfig({
'card-reverse': '1',
},
},
colors: {
'theme-color': '#2080F0',
},
height: {
'side-bar-content': 'calc(var(--side-bar-content))',
},
},
})
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