Vendored
+3
-1
@@ -72,5 +72,7 @@
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"cSpell.words": ["Chatdoc"]
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,3 +65,9 @@ Artistic QR Code:
|
||||

|
||||
|
||||
<img src="https://cdn.acedata.cloud/l11ebv.png" style="width: 300px">
|
||||
|
||||
Suno Music:
|
||||
|
||||

|
||||
|
||||
<img src="https://cdn.acedata.cloud/4mvwe.png" style="width: 300px">
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "major",
|
||||
"comment": "init suno",
|
||||
"packageName": "@acedatacloud/nexior",
|
||||
"email": "office@acedata.cloud",
|
||||
"dependentChangeType": "patch"
|
||||
}
|
||||
@@ -35,6 +35,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.2",
|
||||
"@icon-park/vue-next": "^1.3.6",
|
||||
"axios": "^1.6.0",
|
||||
"codemirror": "^6.0.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,3 @@
|
||||
import OpticalDisk from './disk.png';
|
||||
|
||||
export { OpticalDisk };
|
||||
@@ -13,7 +13,10 @@
|
||||
<el-menu :collapse="false" class="menu">
|
||||
<el-popover :width="350" trigger="hover">
|
||||
<template #reference>
|
||||
<el-menu-item v-if="site?.features?.support?.wechat?.enabled" index="1">
|
||||
<el-menu-item
|
||||
v-if="site?.features?.support?.wechat?.enabled && site?.features?.support?.wechat?.qr"
|
||||
index="1"
|
||||
>
|
||||
<font-awesome-icon icon="fa-brands fa-weixin" class="mr-2" />
|
||||
<template #title>{{ $t('common.message.addWeChat') }}</template>
|
||||
</el-menu-item>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<Component
|
||||
:is="icon"
|
||||
:theme="theme"
|
||||
:size="size"
|
||||
:spin="spin"
|
||||
:fill="fill"
|
||||
:stroke-linecap="strokeLinecap"
|
||||
:stroke-linejoin="strokeLinejoin"
|
||||
:stroke-width="strokeWidth"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Icon } from '@icon-park/vue-next/lib/runtime';
|
||||
|
||||
defineProps<{
|
||||
icon: Icon;
|
||||
theme?: 'outline' | 'filled' | 'two-tone' | 'multi-color';
|
||||
size?: number | string;
|
||||
spin?: boolean;
|
||||
fill?: string | string[];
|
||||
strokeLinecap?: 'butt' | 'round' | 'square';
|
||||
strokeLinejoin?: 'miter' | 'round' | 'bevel';
|
||||
strokeWidth?: number;
|
||||
}>();
|
||||
</script>
|
||||
@@ -108,7 +108,9 @@ import {
|
||||
ROUTE_INDEX,
|
||||
ROUTE_MIDJOURNEY_INDEX,
|
||||
ROUTE_QRART_INDEX,
|
||||
ROUTE_SUNO_INDEX,
|
||||
ROUTE_QRART_HISTORY,
|
||||
ROUTE_SUNO_HISTORY,
|
||||
ROUTE_SITE_INDEX
|
||||
} from '@/router/constants';
|
||||
import LogoTiny from './LogoTiny.vue';
|
||||
@@ -164,6 +166,7 @@ export default defineComponent({
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
if (this.$store?.state?.site?.features?.chatdoc?.enabled) {
|
||||
result.push({
|
||||
route: {
|
||||
@@ -174,6 +177,7 @@ export default defineComponent({
|
||||
routes: [ROUTE_CHATDOC_INDEX, ROUTE_CHATDOC_CONVERSATION, ROUTE_CHATDOC_MANAGE, ROUTE_CHATDOC_SETTING]
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
if (this.$store?.state?.site?.features?.qrart?.enabled) {
|
||||
result.push({
|
||||
@@ -185,6 +189,17 @@ export default defineComponent({
|
||||
routes: [ROUTE_QRART_INDEX, ROUTE_QRART_HISTORY]
|
||||
});
|
||||
}
|
||||
// Add suno's leftmost icon
|
||||
if (this.$store?.state?.site?.features?.suno?.enabled) {
|
||||
result.push({
|
||||
route: {
|
||||
name: ROUTE_SUNO_INDEX
|
||||
},
|
||||
displayName: this.$t('common.nav.suno'),
|
||||
icon: 'fa-solid fa-music',
|
||||
routes: [ROUTE_SUNO_INDEX, ROUTE_SUNO_HISTORY]
|
||||
});
|
||||
}
|
||||
|
||||
if (this.direction === 'row') {
|
||||
result.push({
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
index="/midjourney"
|
||||
></el-menu-item>
|
||||
<el-menu-item v-if="site?.features?.qrart?.enabled" v-t="'index.title.qrart'" index="/qrart"></el-menu-item>
|
||||
<el-menu-item v-if="site?.features?.suno?.enabled" v-t="'index.title.suno'" index="/suno"></el-menu-item>
|
||||
</el-menu>
|
||||
</el-col>
|
||||
<el-col :md="4" :xs="11">
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="config">
|
||||
<type-selector class="mb-4" />
|
||||
<instrument-switch class="mb-4 instrument" />
|
||||
<prompt-input v-if="!config?.custom" class="mb-4" />
|
||||
<lyric-input v-if="config?.custom && !config.instrumental" class="mb-4" />
|
||||
<style-input v-if="config?.custom" class="mb-4" />
|
||||
<title-input v-if="config?.custom" class="mb-4" />
|
||||
<div class="actions">
|
||||
<el-button type="primary" class="btn w-full" round @click="onGenerate">
|
||||
<font-awesome-icon icon="fa-solid fa-magic" class="mr-2" />
|
||||
{{ $t('suno.button.generate') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { ElButton } from 'element-plus';
|
||||
import TypeSelector from './config/TypeSelector.vue';
|
||||
import PromptInput from './config/PromptInput.vue';
|
||||
import LyricInput from './config/LyricInput.vue';
|
||||
import StyleInput from './config/StyleInput.vue';
|
||||
import TitleInput from './config/TitleInput.vue';
|
||||
import InstrumentSwitch from './config/InstrumentSwitch.vue';
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PresetPanel',
|
||||
components: {
|
||||
TypeSelector,
|
||||
PromptInput,
|
||||
LyricInput,
|
||||
InstrumentSwitch,
|
||||
StyleInput,
|
||||
TitleInput,
|
||||
FontAwesomeIcon,
|
||||
ElButton
|
||||
},
|
||||
emits: ['generate'],
|
||||
computed: {
|
||||
config() {
|
||||
return this.$store.state.suno?.config;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onGenerate() {
|
||||
this.$emit('generate');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel {
|
||||
height: 100%;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.config {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
flex: 1;
|
||||
position: relative;
|
||||
.instrument {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: -10px;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
.actions {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<el-scrollbar class="right-panel">
|
||||
<div v-if="audio?.object" class="card">
|
||||
<div class="image-container">
|
||||
<el-image :src="audio.image_url" fit="cover">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon><icon-picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<h2 class="title">{{ audio?.title }}</h2>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="artist">
|
||||
<el-avatar :size="30" :src="audio?.image_url"></el-avatar>
|
||||
<span>{{ audio?.prompt }}</span>
|
||||
</div>
|
||||
<p class="style">{{ audio?.style }}</p>
|
||||
<p class="time">{{ $dayjs.format(audio?.created_at) }}</p>
|
||||
<div class="lyrics">
|
||||
<p>{{ audio?.lyric }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="gradient-background"></div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { ElImage, ElScrollbar, ElAvatar, ElIcon } from 'element-plus';
|
||||
import { Picture as IconPicture } from '@element-plus/icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TaskPreview',
|
||||
components: {
|
||||
ElScrollbar,
|
||||
IconPicture,
|
||||
ElImage,
|
||||
ElAvatar,
|
||||
ElIcon
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCloseIcon: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
audio() {
|
||||
return this.$store.state.suno?.audio;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeCard() {
|
||||
this.showCloseIcon = !this.showCloseIcon;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.right-panel {
|
||||
padding: 2px;
|
||||
background-color: (--el-bg-color);
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.card-scrollbar {
|
||||
height: 600px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.image-container .el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
padding: 5px;
|
||||
color: white;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0;
|
||||
padding: 15px;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0));
|
||||
color: white;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.artist {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.artist span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.lyrics {
|
||||
margin: 15px 0;
|
||||
font-size: 14px;
|
||||
line-height: 25px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="panel recent">
|
||||
<div v-if="tasks?.items === undefined" class="tasks">
|
||||
<div v-for="_ in 3" :key="_" class="task placeholder">
|
||||
<div class="left">
|
||||
<el-skeleton animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="image" class="avatar" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</div>
|
||||
<div class="main">
|
||||
<el-skeleton animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="p" class="title" />
|
||||
<el-skeleton-item variant="text" style="margin-right: 16px" />
|
||||
<el-skeleton-item variant="text" style="width: 80%" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tasks?.items?.length && tasks?.items?.length > 0" class="tasks">
|
||||
<task-preview v-for="(task, taskId) in tasks?.items" :key="taskId" :model-value="task" class="preview" />
|
||||
</div>
|
||||
<p v-if="tasks?.items?.length === 0" class="description">
|
||||
{{ $t('suno.message.noTasks') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-show="!!$store?.state?.suno?.audio?.object" class="flex-1 flex flex-col">
|
||||
<div class="h-20">
|
||||
<player />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import TaskPreview from './task/Preview.vue';
|
||||
import Player from '@/components/suno/player/Player.vue';
|
||||
import { ElSkeleton, ElSkeletonItem } from 'element-plus';
|
||||
// import { ISunoAudio, ISunoAudioLyric, ISunoTask } from '@/models';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RecentPanel',
|
||||
components: {
|
||||
TaskPreview,
|
||||
ElSkeleton,
|
||||
ElSkeletonItem,
|
||||
Player
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
job: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tasks() {
|
||||
// reverse the order of the tasks.items
|
||||
return {
|
||||
...this.$store.state.suno?.tasks,
|
||||
items: this.$store.state.suno?.tasks?.items?.slice().reverse()
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
// justify-content: center;
|
||||
// height: 100%;
|
||||
&.recent {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
.preview {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.description {
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.tasks {
|
||||
width: 100%;
|
||||
.task {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
text-align: left;
|
||||
|
||||
&.placeholder {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
.left {
|
||||
width: 70px;
|
||||
padding: 10px;
|
||||
|
||||
.avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
// border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
width: calc(100% - 70px);
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
height: 200px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operations {
|
||||
height: fit-content !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<el-switch v-model="instrumental" class="value mr-2" />
|
||||
<h2 class="title inline-block">{{ $t('suno.name.instrumental') }}</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { ElSwitch } from 'element-plus';
|
||||
|
||||
export const DEFAULT_INSTRUMENTAL = false;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'InstrumentSwitch',
|
||||
components: {
|
||||
ElSwitch
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
instrumental: {
|
||||
get() {
|
||||
return this.$store.state.suno?.config?.instrumental;
|
||||
},
|
||||
set(val) {
|
||||
console.debug('set instrumental', val);
|
||||
this.$store.commit('suno/setConfig', {
|
||||
...this.$store.state.suno?.config,
|
||||
instrumental: val
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.instrumental === undefined) {
|
||||
this.instrumental = DEFAULT_INSTRUMENTAL;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 120px;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<div class="box">
|
||||
<h2 class="title">{{ $t('suno.name.lyrics') }}</h2>
|
||||
<info-icon :content="$t('suno.description.lyrics')" class="info" />
|
||||
</div>
|
||||
<el-input
|
||||
v-model="lyric"
|
||||
:rows="3"
|
||||
type="textarea"
|
||||
class="lyrics"
|
||||
:placeholder="$t('suno.placeholder.lyrics')"
|
||||
:maxlength="3000"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { ElInput } from 'element-plus';
|
||||
import InfoIcon from '@/components/common/InfoIcon.vue';
|
||||
|
||||
export const DEFAULT_LYRIC = '';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LyricInput',
|
||||
components: {
|
||||
ElInput,
|
||||
InfoIcon
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
lyric: {
|
||||
get() {
|
||||
return this.$store.state.suno?.config?.lyric;
|
||||
},
|
||||
set(val) {
|
||||
console.debug('set lyric', val);
|
||||
this.$store.commit('suno/setConfig', {
|
||||
...this.$store.state.suno?.config,
|
||||
lyric: val
|
||||
});
|
||||
}
|
||||
},
|
||||
config() {
|
||||
return this.$store.state.suno?.config;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.value) {
|
||||
this.value = DEFAULT_LYRIC;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.field {
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<div class="box">
|
||||
<h2 class="title">{{ $t('suno.name.prompt') }}</h2>
|
||||
<info-icon :content="$t('suno.description.prompt')" class="info" />
|
||||
</div>
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
:rows="3"
|
||||
type="textarea"
|
||||
class="prompt"
|
||||
:placeholder="$t('suno.placeholder.prompt')"
|
||||
:maxlength="300"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { ElInput } from 'element-plus';
|
||||
import InfoIcon from '@/components/common/InfoIcon.vue';
|
||||
|
||||
export const DEFAULT_PROMPT = '';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PromptInput',
|
||||
components: {
|
||||
ElInput,
|
||||
InfoIcon
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
prompt: {
|
||||
get() {
|
||||
return this.$store.state.suno?.config?.prompt;
|
||||
},
|
||||
set(val) {
|
||||
console.debug('set prompt', val);
|
||||
this.$store.commit('suno/setConfig', {
|
||||
...this.$store.state.suno?.config,
|
||||
prompt: val
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.value) {
|
||||
this.value = DEFAULT_PROMPT;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.field {
|
||||
.box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<div class="title-container">
|
||||
<h2 class="title">{{ $t('suno.name.style') }}</h2>
|
||||
<info-icon :content="$t('suno.description.style')" class="info" />
|
||||
</div>
|
||||
<el-input
|
||||
v-model="style"
|
||||
:rows="3"
|
||||
type="textarea"
|
||||
class="style"
|
||||
:placeholder="$t('suno.placeholder.style')"
|
||||
:maxlength="300"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { ElInput } from 'element-plus';
|
||||
import InfoIcon from '@/components/common/InfoIcon.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'StyleInput',
|
||||
components: {
|
||||
ElInput,
|
||||
InfoIcon
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
style: {
|
||||
get() {
|
||||
return this.$store.state.suno?.config?.style;
|
||||
},
|
||||
set(val) {
|
||||
console.debug('set style', val);
|
||||
this.$store.commit('suno/setConfig', {
|
||||
...this.$store.state.suno?.config,
|
||||
style: val
|
||||
});
|
||||
}
|
||||
},
|
||||
config() {
|
||||
return this.$store.state.suno?.config;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.field {
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.right-aligned-switch {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<div class="title-container">
|
||||
<h2 class="title">{{ $t('suno.name.title') }}</h2>
|
||||
<info-icon :content="$t('suno.description.title')" class="info" />
|
||||
</div>
|
||||
<el-input
|
||||
v-model="title"
|
||||
:rows="3"
|
||||
type="textarea"
|
||||
class="title"
|
||||
:placeholder="$t('suno.placeholder.title')"
|
||||
:maxlength="300"
|
||||
show-word-limit
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { ElInput } from 'element-plus';
|
||||
import InfoIcon from '@/components/common/InfoIcon.vue';
|
||||
|
||||
export const DEFAULT_PROMPT = '';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TitleInput',
|
||||
components: {
|
||||
ElInput,
|
||||
InfoIcon
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
title: {
|
||||
get() {
|
||||
return this.$store.state.suno?.config?.title;
|
||||
},
|
||||
set(val) {
|
||||
console.debug('set title', val);
|
||||
this.$store.commit('suno/setConfig', {
|
||||
...this.$store.state.suno?.config,
|
||||
title: val
|
||||
});
|
||||
}
|
||||
},
|
||||
instrumental: {
|
||||
get() {
|
||||
return this.$store.state.suno?.config?.instrumental;
|
||||
},
|
||||
set(val) {
|
||||
console.debug('set instrumental', val);
|
||||
this.$store.commit('suno/setConfig', {
|
||||
...this.$store.state.suno?.config,
|
||||
instrumental: val
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.value) {
|
||||
this.value = DEFAULT_PROMPT;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.field {
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
.title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.right-aligned-switch {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<h2 class="title">{{ $t('suno.name.type') }}</h2>
|
||||
<el-switch v-model="custom" class="value" />
|
||||
<el-select v-model="model" class="value" :placeholder="$t('suno.placeholder.select')">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue';
|
||||
import { ElSelect, ElOption, ElSwitch } from 'element-plus';
|
||||
import { SUNO_DEFAULT_MODEL } from '@/constants';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VersionSelector',
|
||||
components: {
|
||||
ElSelect,
|
||||
ElOption,
|
||||
ElSwitch
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
options: [
|
||||
{
|
||||
label: this.$t('suno.model.model1'),
|
||||
value: 'chirp-v2-xxl-alpha'
|
||||
},
|
||||
{
|
||||
label: this.$t('suno.model.model2'),
|
||||
value: 'chirp-v3-0'
|
||||
},
|
||||
{
|
||||
label: this.$t('suno.model.model3'),
|
||||
value: 'chirp-v3-5'
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
custom: {
|
||||
get() {
|
||||
return this.$store.state.suno?.config?.custom;
|
||||
},
|
||||
set(val) {
|
||||
console.debug('set custom', val);
|
||||
this.$store.commit('suno/setConfig', {
|
||||
...this.$store.state.suno?.config,
|
||||
custom: val
|
||||
});
|
||||
}
|
||||
},
|
||||
model: {
|
||||
get() {
|
||||
return this.$store.state.suno?.config?.model;
|
||||
},
|
||||
set(val) {
|
||||
console.debug('set model', val);
|
||||
this.$store.commit('suno/setConfig', {
|
||||
...this.$store.state.suno?.config,
|
||||
model: val
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.model) {
|
||||
console.debug('set default type', SUNO_DEFAULT_MODEL);
|
||||
this.model = SUNO_DEFAULT_MODEL;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-stretch h-20">
|
||||
<player-slider />
|
||||
<div class="flex grow px-5 items-center">
|
||||
<div class="flex-1">
|
||||
<player-song />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<player-controller />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<player-action />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PlayerSlider from './PlayerSlider.vue';
|
||||
import PlayerSong from './PlayerSong.vue';
|
||||
import PlayerAction from './PlayerAction.vue';
|
||||
import PlayerController from './PlayerController.vue';
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="flex justify-end items-center gap-x-2.5">
|
||||
<span class="text-xs"> {{ useFormatDuring(progress) }} / {{ useFormatDuring(duration) }} </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFormatDuring } from '@/utils/number';
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const progress = computed({
|
||||
get: () => store.state.suno?.audio?.progress,
|
||||
set: (value) =>
|
||||
store.commit('suno/setAudio', {
|
||||
...store.state.suno.audio,
|
||||
progress: value
|
||||
})
|
||||
});
|
||||
|
||||
const duration = computed({
|
||||
get: () => store.state.suno?.audio?.duration,
|
||||
set: (value) =>
|
||||
store.commit('suno/setAudio', {
|
||||
...store.state.suno.audio,
|
||||
duration: value
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center gap-x-3">
|
||||
<icon-park
|
||||
:icon="audio?.state === 'playing' ? PauseOne : Play"
|
||||
size="45"
|
||||
theme="filled"
|
||||
class="control"
|
||||
@click="togglePlay"
|
||||
/>
|
||||
<el-popover placement="top" width="50px">
|
||||
<template #reference>
|
||||
<icon-park :icon="VolumeSmall" size="20" :stroke-width="3" class="hover-text" />
|
||||
</template>
|
||||
<player-volume-slider />
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Play, PauseOne, VolumeSmall } from '@icon-park/vue-next';
|
||||
import IconPark from '@/components/common/IconPark.vue';
|
||||
import PlayerVolumeSlider from './PlayerVolumeSlider.vue';
|
||||
import { ElPopover } from 'element-plus';
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
const store = useStore();
|
||||
const togglePlay = () =>
|
||||
store.dispatch('suno/setAudio', {
|
||||
...store.state.suno.audio,
|
||||
state: store.state.suno.audio.state === 'playing' ? 'paused' : 'playing'
|
||||
});
|
||||
const audio = computed(() => store.state.suno.audio);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.control {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="player-slider">
|
||||
<el-slider
|
||||
v-model="progress"
|
||||
:show-tooltip="false"
|
||||
:min="0"
|
||||
:max="duration"
|
||||
@change="onSliderChange"
|
||||
@input="onSliderInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElSlider } from 'element-plus';
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const progress = computed({
|
||||
get: () => store.state.suno?.audio?.progress,
|
||||
set: (value) =>
|
||||
store.commit('suno/setAudio', {
|
||||
...store.state.suno.audio,
|
||||
progress: value
|
||||
})
|
||||
});
|
||||
|
||||
const duration = computed({
|
||||
get: () => store.state.suno?.audio?.duration,
|
||||
set: (value) => store.commit('suno/setAudio', { ...store.state.suno.audio, duration: value })
|
||||
});
|
||||
|
||||
const onSliderInput = () => {};
|
||||
const onSliderChange = (val: number) =>
|
||||
store.dispatch('suno/setAudio', {
|
||||
...store.state.suno.audio,
|
||||
progress: val
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.player-slider {
|
||||
.el-slider {
|
||||
height: 10px;
|
||||
|
||||
.el-slider__runway,
|
||||
.el-slider__bar {
|
||||
height: 2px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.el-slider__runway {
|
||||
//@apply bg-slate-50 dark:bg-stone-700 transition-all;
|
||||
//@apply hover:bg-stone-200 dark:hover:bg-stone-600;
|
||||
}
|
||||
|
||||
.el-slider__button-wrapper {
|
||||
@apply opacity-0 transition-opacity;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: -10.5px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.el-slider__button-wrapper {
|
||||
@apply opacity-100;
|
||||
}
|
||||
}
|
||||
|
||||
.el-slider__button {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@apply bg-emerald-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="flex player-song">
|
||||
<img alt="" class="w-11 h-11 rounded" :src="audio?.image_url || OpticalDisk" />
|
||||
<div class="ml-2 text-xs flex flex-col justify-between">
|
||||
<div class="w-52 2xl:w-96 cursor-pointer truncate">
|
||||
<div class="flex">
|
||||
<span>{{ audio?.title || 'Music' }}</span>
|
||||
<span class="ml-2 text-dc">- {{ audio?.style || `SmallRuralDog` }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { OpticalDisk } from '@/assets/img';
|
||||
import { computed, watch } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
|
||||
const store = useStore();
|
||||
const audio = computed({
|
||||
get: () => store.state.suno.audio,
|
||||
set: (value) => store.commit('suno/setAudio', value)
|
||||
});
|
||||
|
||||
// watch audio change and play
|
||||
watch(audio, (value, oldValue) => {
|
||||
// url changed
|
||||
if (value?.audio_url !== oldValue?.audio_url) {
|
||||
console.log('audio changed', value);
|
||||
if (value.object) {
|
||||
console.log('111', value.object);
|
||||
// delete old object
|
||||
value.object.pause();
|
||||
delete value.object;
|
||||
}
|
||||
const object = new Audio(value.audio_url);
|
||||
object.currentTime = 0;
|
||||
if (value.state === 'playing') {
|
||||
object.play();
|
||||
} else {
|
||||
object.pause();
|
||||
}
|
||||
|
||||
// listen to the time change of audio
|
||||
object.addEventListener('timeupdate', () => {
|
||||
store.commit('suno/setAudio', {
|
||||
...store.state.suno.audio,
|
||||
progress: object.currentTime
|
||||
});
|
||||
});
|
||||
|
||||
store.commit('suno/setAudio', {
|
||||
...store.state.suno.audio,
|
||||
object: object
|
||||
});
|
||||
} else if (value?.progress !== oldValue?.progress && Math.abs(value.progress - value.object.currentTime) > 2) {
|
||||
console.log('progress changed', value.progress);
|
||||
const audio = store.state.suno.audio;
|
||||
if (audio.object) {
|
||||
audio.object.currentTime = audio.progress;
|
||||
}
|
||||
} else if (value?.state !== oldValue?.state) {
|
||||
console.log('state changed', value.state);
|
||||
if (value.object) {
|
||||
if (value.state === 'playing') {
|
||||
value.object.play();
|
||||
} else {
|
||||
value.object.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value?.volume !== oldValue?.volume) {
|
||||
console.log('volume changed', value.volume);
|
||||
if (value.object) {
|
||||
value.object.volume = value.volume / 100;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="player-volume flex flex-col items-center pt-2">
|
||||
<div>
|
||||
<el-slider
|
||||
v-model="volume"
|
||||
vertical
|
||||
height="100px"
|
||||
:show-tooltip="false"
|
||||
:max="100"
|
||||
:min="0"
|
||||
size="small"
|
||||
:disabled="muted"
|
||||
@input="setVolume"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-sm mt-3">{{ volume }}</div>
|
||||
<div class="mt-2">
|
||||
<icon-park
|
||||
:icon="muted ? VolumeMute : VolumeSmall"
|
||||
size="16"
|
||||
theme="filled"
|
||||
class="hover-text"
|
||||
@click="toggleMuted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { VolumeMute, VolumeSmall } from '@icon-park/vue-next';
|
||||
import IconPark from '@/components/common/IconPark.vue';
|
||||
import { ElSlider } from 'element-plus';
|
||||
import { computed } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
const store = useStore();
|
||||
|
||||
const volume = computed({
|
||||
get: () => store.state.suno.audio?.volume,
|
||||
set: (value) => store.commit('suno/setAudio', { ...store.state.suno.audio, volume: value })
|
||||
});
|
||||
|
||||
const muted = computed({
|
||||
get: () => store.state.suno.audio?.muted,
|
||||
set: (value) => store.commit('suno/setAudio', { ...store.state.suno.audio, muted: value })
|
||||
});
|
||||
|
||||
const toggleMuted = () => store.dispatch('suno/toggleMuted');
|
||||
|
||||
const setVolume = (value: number) => store.dispatch('suno/setVolume', value);
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.el-popover.el-popper {
|
||||
min-width: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="task">
|
||||
<div v-for="(audio, audioIndex) in audios" :key="audioIndex" class="audio" @click="onClick(audio)">
|
||||
<div v-loading="!audio?.audio_url" class="left">
|
||||
<el-image :src="audio?.image_url" class="cover" fit="cover" />
|
||||
<div
|
||||
v-if="
|
||||
audio?.audio_url &&
|
||||
$store.state?.suno?.audio?.id === audio.id &&
|
||||
$store.state?.suno?.audio?.state === 'playing'
|
||||
"
|
||||
class="overlay"
|
||||
@click="onPause(audio)"
|
||||
>
|
||||
<el-icon><video-pause /></el-icon>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
audio?.audio_url &&
|
||||
($store.state?.suno?.audio?.id !== audio.id ||
|
||||
($store.state?.suno?.audio?.id === audio.id && $store.state?.suno?.audio?.state === 'paused'))
|
||||
"
|
||||
class="overlay"
|
||||
@click="onPlay(audio)"
|
||||
>
|
||||
<el-icon><video-play /></el-icon>
|
||||
</div>
|
||||
<div v-if="audio?.duration" class="duration">
|
||||
{{ useFormatDuring(audio?.duration) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<h2 class="title">{{ audio?.title }}</h2>
|
||||
<p class="style">{{ audio?.style }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { useFormatDuring } from '@/utils/number';
|
||||
import { ISunoAudio, ISunoTask } from '@/models';
|
||||
import { ElButton, ElImage, ElIcon } from 'element-plus';
|
||||
import { VideoPlay, VideoPause } from '@element-plus/icons-vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TaskPreview',
|
||||
components: {
|
||||
ElImage,
|
||||
ElIcon,
|
||||
VideoPlay,
|
||||
VideoPause
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object as () => ISunoTask,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
task() {
|
||||
return this.$store.state.suno?.tasks;
|
||||
},
|
||||
audios(): ISunoAudio[] {
|
||||
let result: ISunoAudio[] = [];
|
||||
if (Array.isArray(this.modelValue?.response?.data)) {
|
||||
this.modelValue?.response?.data?.forEach((item: any) => {
|
||||
let audio = item as ISunoAudio;
|
||||
result.push(audio);
|
||||
});
|
||||
}
|
||||
console.log('audios', result);
|
||||
return result;
|
||||
},
|
||||
application() {
|
||||
return this.$store.state.suno?.application;
|
||||
},
|
||||
active() {
|
||||
return this.$store.state.suno?.tasks?.active;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
useFormatDuring,
|
||||
onPlay(audio: ISunoAudio) {
|
||||
this.$store.dispatch('suno/setAudio', {
|
||||
...this.$store.state.suno.audio,
|
||||
...audio,
|
||||
state: 'playing'
|
||||
});
|
||||
console.log('on play');
|
||||
},
|
||||
onPause(audio: ISunoAudio) {
|
||||
this.$store.dispatch('suno/setAudio', {
|
||||
...this.$store.state.suno.audio,
|
||||
...audio,
|
||||
state: 'paused'
|
||||
});
|
||||
console.log('on pause');
|
||||
},
|
||||
onClick(audio: ISunoAudio) {
|
||||
if (this.$store.state?.suno?.audio?.id !== audio.id) {
|
||||
this.onPlay({
|
||||
...audio,
|
||||
progress: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.task {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.audio {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.left {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover .overlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.duration {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 2px 4px;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
transition: opacity 0.3s;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
line-height: 70px;
|
||||
cursor: pointer;
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.info {
|
||||
flex: 1;
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.style {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,4 +5,5 @@ export * from './chat';
|
||||
export * from './chatdoc';
|
||||
export * from './midjourney';
|
||||
export * from './qrart';
|
||||
export * from './suno';
|
||||
export * from './mapping';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export const SUNO_SERVICE_ID = 'f2b646d8-3cfd-46ef-969a-1ea9eebde329';
|
||||
|
||||
export const SUNO_DEFAULT_QRW = 2;
|
||||
export const SUNO_DEFAULT_STEPS = 20;
|
||||
export const SUNO_DEFAULT_PRESET = '';
|
||||
export const SUNO_DEFAULT_MODEL = 'chirp-v3-5';
|
||||
export const SUNO_DEFAULT_ASPECT_RATIO = '1:1';
|
||||
export const SUNO_DEFAULT_ADVANCED = false;
|
||||
export const SUNO_DEFAULT_RAWURL = true;
|
||||
export const SUNO_DEFAULT_PADDING_LEVEL = 5;
|
||||
export const SUNO_DEFAULT_POSITION = 'center';
|
||||
export const SUNO_DEFAULT_PIXEL_STYLE = 'square';
|
||||
export const SUNO_DEFAULT_MARKER_SHAPE = 'square';
|
||||
export const SUNO_DEFAULT_SUB_MARKER = 'square';
|
||||
export const SUNO_DEFAULT_ROTATE = 0;
|
||||
export const SUNO_DEFAULT_ECL = 'H';
|
||||
export const SUNO_DEFAULT_PADDING_NOISE = 0;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user