feat:【ele】【ai】音乐的迁移

This commit is contained in:
YunaiV
2025-11-14 22:22:43 +08:00
parent d056629332
commit 1cbd4033b8
10 changed files with 542 additions and 0 deletions

View File

@@ -27,6 +27,7 @@ export function getKnowledge(id: number) {
`/ai/knowledge/get?id=${id}`,
);
}
// 新增知识库
export function createKnowledge(data: AiKnowledgeKnowledgeApi.Knowledge) {
return requestClient.post('/ai/knowledge/create', data);

View File

@@ -0,0 +1,29 @@
<script lang="ts" setup>
import type { Nullable, Recordable } from '@vben/types';
import { ref, unref } from 'vue';
import { Page } from '@vben/common-ui';
import List from './list/index.vue';
import Mode from './mode/index.vue';
defineOptions({ name: 'AiMusicIndex' });
const listRef = ref<Nullable<{ generateMusic: (...args: any) => void }>>(null);
function generateMusic(args: { formData: Recordable<any> }) {
unref(listRef)?.generateMusic(args.formData);
}
</script>
<template>
<Page auto-content-height>
<div class="flex h-full items-stretch">
<!-- 模式 -->
<Mode class="flex-none" @generate-music="generateMusic" />
<!-- 音频列表 -->
<List ref="listRef" class="flex-auto" />
</div>
</Page>
</template>

View File

@@ -0,0 +1,99 @@
<script lang="ts" setup>
import { inject, reactive, ref } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { formatPast } from '@vben/utils';
import { ElImage, ElSlider } from 'element-plus';
defineOptions({ name: 'AiMusicAudioBarIndex' });
const currentSong = inject<any>('currentSong', {});
const audioRef = ref<HTMLAudioElement | null>(null);
const audioProps = reactive<any>({
autoplay: true,
paused: false,
currentTime: '00:00',
duration: '00:00',
muted: false,
volume: 50,
}); // 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
function toggleStatus(type: string) {
audioProps[type] = !audioProps[type];
if (type === 'paused' && audioRef.value) {
if (audioProps[type]) {
audioRef.value.pause();
} else {
audioRef.value.play();
}
}
}
/** 更新播放位置 */
function audioTimeUpdate(args: any) {
audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss');
}
</script>
<template>
<div
class="b-1 b-l-none h-18 bg-card flex items-center justify-between border border-solid border-rose-100 px-2"
>
<!-- 歌曲信息 -->
<div class="flex gap-2.5">
<ElImage
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
class="!w-[45px]"
/>
<div>
<div>{{ currentSong.name }}</div>
<div class="text-xs text-gray-400">{{ currentSong.singer }}</div>
</div>
</div>
<!-- 音频controls -->
<div class="flex items-center gap-3">
<IconifyIcon
icon="majesticons:back-circle"
class="size-5 cursor-pointer text-gray-300"
/>
<IconifyIcon
:icon="
audioProps.paused
? 'mdi:arrow-right-drop-circle'
: 'solar:pause-circle-bold'
"
class="size-7 cursor-pointer"
@click="toggleStatus('paused')"
/>
<IconifyIcon
icon="majesticons:next-circle"
class="size-5 cursor-pointer text-gray-300"
/>
<div class="flex items-center gap-4">
<span>{{ audioProps.currentTime }}</span>
<ElSlider v-model="audioProps.duration" color="#409eff" class="!w-40" />
<span>{{ audioProps.duration }}</span>
</div>
<!-- 音频 -->
<audio
v-bind="audioProps"
ref="audioRef"
controls
v-show="!audioProps"
@timeupdate="audioTimeUpdate"
>
<!-- <source :src="audioUrl" /> -->
</audio>
</div>
<div class="flex items-center gap-4">
<IconifyIcon
:icon="audioProps.muted ? 'tabler:volume-off' : 'tabler:volume'"
class="size-5 cursor-pointer"
@click="toggleStatus('muted')"
/>
<ElSlider v-model="audioProps.volume" color="#409eff" class="!w-40" />
</div>
</div>
</template>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import { provide, ref } from 'vue';
import { ElCol, ElEmpty, ElRow, ElTabPane, ElTabs } from 'element-plus';
import audioBar from './audioBar/index.vue';
import songCard from './songCard/index.vue';
import songInfo from './songInfo/index.vue';
defineOptions({ name: 'AiMusicListIndex' });
const currentType = ref('mine');
const loading = ref(false); // loading 状态
const currentSong = ref({}); // 当前音乐
const mySongList = ref<Recordable<any>[]>([]);
const squareSongList = ref<Recordable<any>[]>([]);
function generateMusic(formData: Recordable<any>) {
loading.value = true;
setTimeout(() => {
mySongList.value = Array.from({ length: 20 }, (_, index) => {
return {
id: index,
audioUrl: '',
videoUrl: '',
title: `我走后${index}`,
imageUrl:
'https://www.carsmp3.com/data/attachment/forum/201909/19/091020q5kgre20fidreqyt.jpg',
desc: 'Metal, symphony, film soundtrack, grand, majesticMetal, dtrack, grand, majestic',
date: '2024年04月30日 14:02:57',
lyric: `<div class="_words_17xen_66"><div>大江东去,浪淘尽,千古风流人物。
</div><div>故垒西边,人道是,三国周郎赤壁。
</div><div>乱石穿空,惊涛拍岸,卷起千堆雪。
</div><div>江山如画,一时多少豪杰。
</div><div>
</div><div>遥想公瑾当年,小乔初嫁了,雄姿英发。
</div><div>羽扇纶巾,谈笑间,樯橹灰飞烟灭。
</div><div>故国神游,多情应笑我,早生华发。
</div><div>人生如梦,一尊还酹江月。</div></div>`,
};
});
loading.value = false;
}, 3000);
}
function setCurrentSong(music: Recordable<any>) {
currentSong.value = music;
}
defineExpose({
generateMusic,
});
provide('currentSong', currentSong);
</script>
<template>
<div class="flex flex-col">
<div class="flex flex-auto overflow-hidden">
<ElTabs
v-model="currentType"
class="flex-auto px-5"
tab-position="bottom"
>
<!-- 我的创作 -->
<ElTabPane name="mine" label="我的创作" v-loading="loading">
<ElRow v-if="mySongList.length > 0" :gutter="12">
<ElCol v-for="song in mySongList" :key="song.id" :span="24">
<songCard :song-info="song" @play="setCurrentSong(song)" />
</ElCol>
</ElRow>
<ElEmpty v-else description="暂无音乐" />
</ElTabPane>
<!-- 试听广场 -->
<ElTabPane name="square" label="试听广场" v-loading="loading">
<ElRow v-if="squareSongList.length > 0" :gutter="12">
<ElCol v-for="song in squareSongList" :key="song.id" :span="24">
<songCard :song-info="song" @play="setCurrentSong(song)" />
</ElCol>
</ElRow>
<ElEmpty v-else description="暂无音乐" />
</ElTabPane>
</ElTabs>
<!-- songInfo -->
<songInfo class="flex-none" />
</div>
<audioBar class="flex-none" />
</div>
</template>
<style lang="scss" scoped>
:deep(.el-tabs) {
.el-tabs__content {
padding: 0 7px;
overflow: auto;
}
}
</style>

View File

@@ -0,0 +1,50 @@
<script lang="ts" setup>
import { inject } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { ElImage } from 'element-plus';
defineOptions({ name: 'AiMusicSongCardIndex' });
defineProps({
songInfo: {
type: Object,
default: () => ({}),
},
});
const emits = defineEmits(['play']);
const currentSong = inject<any>('currentSong', {});
function playSong() {
emits('play');
}
</script>
<template>
<div class="mb-3 flex rounded p-3">
<div class="relative" @click="playSong">
<ElImage :src="songInfo.imageUrl" class="w-20 flex-none" />
<div
class="absolute left-0 top-0 flex h-full w-full cursor-pointer items-center justify-center bg-black bg-opacity-40"
>
<IconifyIcon
:icon="
currentSong.id === songInfo.id
? 'solar:pause-circle-bold'
: 'mdi:arrow-right-drop-circle'
"
:size="30"
/>
</div>
</div>
<div class="ml-2">
<div>{{ songInfo.title }}</div>
<div class="mt-2 line-clamp-2 text-xs">
{{ songInfo.desc }}
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,25 @@
<script lang="ts" setup>
import { inject } from 'vue';
import { ElButton, ElCard, ElImage } from 'element-plus';
defineOptions({ name: 'AiMusicSongInfoIndex' });
const currentSong = inject<any>('currentSong', {});
</script>
<template>
<ElCard class="!mb-0 w-40 leading-6">
<ElImage :src="currentSong.imageUrl" class="h-full w-full" />
<div class="">{{ currentSong.title }}</div>
<div class="line-clamp-1 text-xs">
{{ currentSong.desc }}
</div>
<div class="text-xs">
{{ currentSong.date }}
</div>
<ElButton size="small" round class="my-2">信息复用</ElButton>
<div class="text-xs" v-html="currentSong.lyric"></div>
</ElCard>
</template>

View File

@@ -0,0 +1,66 @@
<script lang="ts" setup>
import { reactive } from 'vue';
import { ElInput, ElOption, ElSelect, ElSwitch } from 'element-plus';
import Title from '../title/index.vue';
defineOptions({ name: 'AiMusicModeDesc' });
const formData = reactive({
desc: '',
pure: false,
version: '3',
});
defineExpose({
formData,
});
</script>
<template>
<div>
<Title
title="音乐/歌词说明"
desc="描述您想要的音乐风格和主题,使用流派和氛围而不是特定的艺术家和歌曲"
>
<ElInput
v-model="formData.desc"
type="textarea"
:autosize="{ minRows: 6, maxRows: 6 }"
:maxlength="1200"
:show-word-limit="true"
placeholder="一首关于糟糕分手的欢快歌曲"
/>
</Title>
<Title title="纯音乐" class="mt-5" desc="创建一首没有歌词的歌曲">
<template #extra>
<ElSwitch v-model="formData.pure" size="small" />
</template>
</Title>
<Title
title="版本"
desc="描述您想要的音乐风格和主题,使用流派和氛围而不是特定的艺术家和歌曲"
>
<ElSelect v-model="formData.version" class="w-full" placeholder="请选择">
<ElOption
v-for="item in [
{
value: '3',
label: 'V3',
},
{
value: '2',
label: 'V2',
},
]"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</ElSelect>
</Title>
</div>
</template>

View File

@@ -0,0 +1,38 @@
<script lang="ts" setup>
import type { Nullable, Recordable } from '@vben/types';
import { ref, unref } from 'vue';
import { ElButton, ElCard, ElRadioButton, ElRadioGroup } from 'element-plus';
import desc from './desc.vue';
import lyric from './lyric.vue';
defineOptions({ name: 'AiMusicModeIndex' });
const emits = defineEmits(['generateMusic']);
const generateMode = ref('lyric');
const modeRef = ref<Nullable<{ formData: Recordable<any> }>>(null);
function generateMusic() {
emits('generateMusic', { formData: unref(modeRef)?.formData });
}
</script>
<template>
<ElCard class="!mb-0 h-full w-80">
<ElRadioGroup v-model="generateMode" class="mb-4">
<ElRadioButton value="desc"> 描述模式 </ElRadioButton>
<ElRadioButton value="lyric"> 歌词模式 </ElRadioButton>
</ElRadioGroup>
<!-- 描述模式/歌词模式 切换 -->
<component :is="generateMode === 'desc' ? desc : lyric" ref="modeRef" />
<ElButton type="primary" round class="w-full" @click="generateMusic">
创作音乐
</ElButton>
</ElCard>
</template>

View File

@@ -0,0 +1,107 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import {
ElButton,
ElInput,
ElOption,
ElSelect,
ElSpace,
ElTag,
} from 'element-plus';
import Title from '../title/index.vue';
defineOptions({ name: 'AiMusicModeLyric' });
const tags = ['rock', 'punk', 'jazz', 'soul', 'country', 'kidsmusic', 'pop'];
const showCustom = ref(false);
const formData = reactive({
lyric: '',
style: '',
name: '',
version: '',
});
defineExpose({
formData,
});
</script>
<template>
<div class="">
<Title title="歌词" desc="自己编写歌词或使用Ai生成歌词两节/8行效果最佳">
<ElInput
v-model="formData.lyric"
type="textarea"
:autosize="{ minRows: 6, maxRows: 6 }"
:maxlength="1200"
:show-word-limit="true"
placeholder="请输入您自己的歌词"
/>
</Title>
<Title title="音乐风格">
<ElSpace class="flex-wrap">
<ElTag v-for="tag in tags" :key="tag" class="mb-2">
{{ tag }}
</ElTag>
</ElSpace>
<ElButton
:type="showCustom ? 'primary' : 'default'"
round
size="small"
class="mb-2"
@click="showCustom = !showCustom"
>
自定义风格
</ElButton>
</Title>
<Title
v-show="showCustom"
desc="描述您想要的音乐风格Suno无法识别艺术家的名字但可以理解流派和氛围"
class="mt-3"
>
<ElInput
v-model="formData.style"
type="textarea"
:autosize="{ minRows: 4, maxRows: 4 }"
:maxlength="256"
show-word-limit
placeholder="输入音乐风格(英文)"
/>
</Title>
<Title title="音乐/歌曲名称">
<ElInput
class="w-full"
v-model="formData.name"
placeholder="请输入音乐/歌曲名称"
/>
</Title>
<Title title="版本">
<ElSelect v-model="formData.version" class="w-full" placeholder="请选择">
<ElOption
v-for="item in [
{
value: '3',
label: 'V3',
},
{
value: '2',
label: 'V2',
},
]"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</ElSelect>
</Title>
</div>
</template>

View File

@@ -0,0 +1,27 @@
<script lang="ts" setup>
defineOptions({ name: 'AiMusicTitleIndex' });
defineProps({
title: {
type: String,
default: '',
},
desc: {
type: String,
default: '',
},
});
</script>
<template>
<div class="mb-3">
<div class="flex items-center justify-between text-gray-600">
<span>{{ title }}</span>
<slot name="extra"></slot>
</div>
<div class="my-2 text-xs text-gray-400">
{{ desc }}
</div>
<slot></slot>
</div>
</template>