feat(ai): 新增 AI 绘图功能
- 添加 AI 绘图相关的 API 接口和路由 - 实现 AI 绘图页面,支持不同平台的绘图功能 - 添加绘图作品列表和重新生成功能 - 优化绘图页面样式和布局
This commit is contained in:
@@ -5,6 +5,7 @@ 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: 'Index' });
|
||||
|
||||
116
apps/web-antd/src/views/ai/music/index/list/audioBar/index.vue
Normal file
116
apps/web-antd/src/views/ai/music/index/list/audioBar/index.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Nullable } from '@vben/types';
|
||||
|
||||
import { inject, reactive, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Image, Slider } from 'ant-design-vue';
|
||||
|
||||
import { formatPast } from '#/utils/formatTime';
|
||||
|
||||
defineOptions({ name: 'Index' });
|
||||
|
||||
const currentSong = inject('currentSong', {});
|
||||
|
||||
const audioRef = ref<Nullable<HTMLElement>>(null);
|
||||
// 音频相关属性https://www.runoob.com/tags/ref-av-dom.html
|
||||
const audioProps = reactive<any>({
|
||||
autoplay: true,
|
||||
paused: false,
|
||||
currentTime: '00:00',
|
||||
duration: '00:00',
|
||||
muted: false,
|
||||
volume: 50,
|
||||
});
|
||||
|
||||
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-solid b-1 b-l-none flex h-[72px] items-center justify-between px-2"
|
||||
style="background-color: #fffffd; border-color: #dcdfe6"
|
||||
>
|
||||
<!-- 歌曲信息 -->
|
||||
<div class="flex gap-[10px]">
|
||||
<Image
|
||||
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
|
||||
:width="45"
|
||||
/>
|
||||
<div>
|
||||
<div>{{ currentSong.name }}</div>
|
||||
<div class="text-[12px] text-gray-400">{{ currentSong.singer }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 音频controls -->
|
||||
<div class="flex items-center gap-[12px]">
|
||||
<IconifyIcon
|
||||
icon="majesticons:back-circle"
|
||||
:size="20"
|
||||
class="cursor-pointer text-gray-300"
|
||||
/>
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
audioProps.paused
|
||||
? 'mdi:arrow-right-drop-circle'
|
||||
: 'solar:pause-circle-bold'
|
||||
"
|
||||
:size="30"
|
||||
class="cursor-pointer"
|
||||
@click="toggleStatus('paused')"
|
||||
/>
|
||||
<IconifyIcon
|
||||
icon="majesticons:next-circle"
|
||||
:size="20"
|
||||
class="cursor-pointer text-gray-300"
|
||||
/>
|
||||
<div class="flex items-center gap-[16px]">
|
||||
<span>{{ audioProps.currentTime }}</span>
|
||||
<Slider
|
||||
v-model:value="audioProps.duration"
|
||||
color="#409eff"
|
||||
class="w-[160px!important]"
|
||||
/>
|
||||
<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-[16px]">
|
||||
<IconifyIcon
|
||||
:icon="audioProps.muted ? 'tabler:volume-off' : 'tabler:volume'"
|
||||
:size="20"
|
||||
class="cursor-pointer"
|
||||
@click="toggleStatus('muted')"
|
||||
/>
|
||||
<Slider
|
||||
v-model:value="audioProps.volume"
|
||||
color="#409eff"
|
||||
class="w-[160px!important]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
113
apps/web-antd/src/views/ai/music/index/list/index.vue
Normal file
113
apps/web-antd/src/views/ai/music/index/list/index.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import { provide, ref } from 'vue';
|
||||
|
||||
import { Col, Empty, Row, TabPane, Tabs } from 'ant-design-vue';
|
||||
|
||||
import audioBar from './audioBar/index.vue';
|
||||
import songCard from './songCard/index.vue';
|
||||
import songInfo from './songInfo/index.vue';
|
||||
|
||||
defineOptions({ name: 'Index' });
|
||||
|
||||
const currentType = ref('mine');
|
||||
// loading 状态
|
||||
const loading = ref(false);
|
||||
// 当前音乐
|
||||
const currentSong = ref({});
|
||||
|
||||
const mySongList = ref<Recordable<any>[]>([]);
|
||||
const squareSongList = ref<Recordable<any>[]>([]);
|
||||
|
||||
/*
|
||||
*@Description: 调接口生成音乐列表
|
||||
*@MethodAuthor: xiaohong
|
||||
*@Date: 2024-06-27 17:06:44
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/*
|
||||
*@Description: 设置当前播放的音乐
|
||||
*@MethodAuthor: xiaohong
|
||||
*@Date: 2024-07-19 11:22:33
|
||||
*/
|
||||
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">
|
||||
<Tabs
|
||||
v-model:active-key="currentType"
|
||||
class="flex-auto px-[20px]"
|
||||
tab-position="bottom"
|
||||
>
|
||||
<!-- 我的创作 -->
|
||||
<TabPane key="mine" tab="我的创作" v-loading="loading">
|
||||
<Row v-if="mySongList.length > 0" :gutter="12">
|
||||
<Col v-for="song in mySongList" :key="song.id" :span="24">
|
||||
<songCard :song-info="song" @play="setCurrentSong(song)" />
|
||||
</Col>
|
||||
</Row>
|
||||
<Empty v-else description="暂无音乐" />
|
||||
</TabPane>
|
||||
|
||||
<!-- 试听广场 -->
|
||||
<TabPane key="square" tab="试听广场" v-loading="loading">
|
||||
<Row v-if="squareSongList.length > 0" :gutter="12">
|
||||
<Col v-for="song in squareSongList" :key="song.id" :span="24">
|
||||
<songCard :song-info="song" @play="setCurrentSong(song)" />
|
||||
</Col>
|
||||
</Row>
|
||||
<Empty v-else description="暂无音乐" />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
<!-- songInfo -->
|
||||
<songInfo class="flex-none" />
|
||||
</div>
|
||||
<audioBar class="flex-none" />
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.ant-tabs) {
|
||||
.ant-tabs__content {
|
||||
padding: 0 7px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Image } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'Index' });
|
||||
|
||||
defineProps({
|
||||
songInfo: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['play']);
|
||||
|
||||
const currentSong = inject('currentSong', {});
|
||||
|
||||
function playSong() {
|
||||
emits('play');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-1 mb-[12px] flex p-[12px]">
|
||||
<div class="relative" @click="playSong">
|
||||
<Image :src="songInfo.imageUrl" class="w-80px flex-none" />
|
||||
<div
|
||||
class="bg-op-40 absolute left-0 top-0 flex h-full w-full cursor-pointer items-center justify-center bg-black"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
currentSong.id === songInfo.id
|
||||
? 'solar:pause-circle-bold'
|
||||
: 'mdi:arrow-right-drop-circle'
|
||||
"
|
||||
:size="30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-[8px]">
|
||||
<div>{{ songInfo.title }}</div>
|
||||
<div class="mt-[8px] line-clamp-2 text-[12px]">
|
||||
{{ songInfo.desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
import { Button, Card, Image } from 'ant-design-vue';
|
||||
|
||||
defineOptions({ name: 'Index' });
|
||||
|
||||
const currentSong = inject('currentSong', {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="line-height-24px mb-[0!important] w-[300px]">
|
||||
<Image :src="currentSong.imageUrl" style="width: 100%; height: 100%" />
|
||||
|
||||
<div class="">{{ currentSong.title }}</div>
|
||||
<div class="line-clamp-1 text-[12px]">
|
||||
{{ currentSong.desc }}
|
||||
</div>
|
||||
<div class="text-[12px]">
|
||||
{{ currentSong.date }}
|
||||
</div>
|
||||
<Button size="small" shape="round" class="my-[6px]">信息复用</Button>
|
||||
<div class="text-[12px]" v-html="currentSong.lyric"></div>
|
||||
</Card>
|
||||
</template>
|
||||
Reference in New Issue
Block a user