the ai video generating for KeLing is ready

This commit is contained in:
RockYang
2025-03-05 14:19:20 +08:00
parent b1fb16995a
commit f580f671a3
10 changed files with 754 additions and 1347 deletions

View File

@@ -1,593 +0,0 @@
.page-keling {
display: flex;
min-height: 100vh;
:deep(.el-form-item__label) {
color: var(--text-theme-color);
}
.grid-content {
// background-color #383838
background: var(--card-bg);
border-radius: 8px;
padding: 8px 14px;
display: flex;
cursor: pointer;
margin-bottom: 10px;
// border 1px solid #383838
border: 1px solid var(--chat-bg);
&:hover {
border: 1px solid var(--theme-border-hover);
}
.icon {
width: 20px;
height: 20px;
margin-bottom: 5px;
}
.texts {
margin-left: 5px;
margin-top: 2px;
color: var(--text-theme-color);
}
}
.param-line.pt {
padding-top: 5px;
padding-bottom: 5px;
}
.grid-content.active {
// color #47fff1
// background-color #585858
border: 1px solid var(--theme-border-hover);
}
.h-20 {
height: 4rem !important;
}
.scrollbar-type-nav {
display: flex;
align-items: center;
padding: 2px;
background-color: var(--tab-title-bg);
width: fit-content;
border: 1px solid rgba(79, 89, 102, 0.078);
border-radius: 20px;
margin: 0 auto;
// background: var(--chat-bg);
// width 100%
li {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
margin: 5px 8px;
height: 26px;
border-radius: 4px;
// border 1px solid rgb(80,80,80)
padding: 2px 12px;
// background rgba(60,60,60 0.9)
color: var(--theme-text-tertiary);
font-weight: bold;
font-size: 14px;
cursor: pointer;
.image {
width: 22px;
height: 22px;
overflow: hidden;
margin-right: 5px;
border-radius: 50%;
}
span {
color: var(--tab-title-color);
}
.el-icon {
fill: var(--tab-title-color);
color: var(--tab-title-color);
}
&.active {
background: #fff;
color: var(--el-color-primary);
border-radius: 20px;
span {
color: var(--el-color-primary);
}
.el-icon {
fill: var(--el-color-primary);
color: var(--el-color-primary) !important;
}
}
}
}
.main-content {
padding-right: 1.5rem;
padding-left: 1.5rem;
padding-bottom: 1rem;
flex: 1;
background: var(--chat-bg);
// width: 100%;
// padding 0 10px 10px 10px
color: var(--text-theme-color);
overflow-x: hidden;
}
.camera-control {
padding: 10px;
border-radius: 4px;
background: var(--card-bg);
:deep(.el-form-item:last-child) {
margin-bottom: 0 !important;
}
}
.title-tabs {
:deep(.el-tabs__item.is-active) {
color: var(--theme-textcolor-normal);
font-size: 18px;
}
:deep(.el-tabs__item) {
color: var(--text-theme-color);
font-size: 18px;
}
}
.el-tabs {
--el-tabs-header-height: 55px;
}
.el-tabs__item {
color: var(--text-theme-color);
font-size: 18px;
}
.el-tabs__item.is-active, .title-tabs .el-tabs__item.is-active {
}
.title-tabs .el-tabs__active-bar {
background-color: var(--theme-textcolor-normal);
}
:deep(.el-textarea) {
--el-input-focus-border-color: var(--el-color-primary);
}
:deep(.el-textarea__inner) {
background: transparent;
color: var(--text-theme-color);
}
.el-input__wrapper {
background: transparent;
padding: 5px;
}
.text {
margin-bottom: 10px;
color: #6b778c;
font-size: 15px;
}
.param-line.pt {
padding-top: 5px;
padding-bottom: 5px;
}
.form-item-inner {
display: flex;
align-items: center;
.el-icon {
margin-left: 10px;
}
}
.el-form-item__label {
color: var(--text-theme-color);
}
//
.img-inline {
display: flex;
gap: 20px;
align-items: center;
.img-uploader {
text-align: center;
:deep(.el-upload) {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 120px;
height: 120px;
line-height: 120px;
transition: var(--el-transition-duration-fast);
margin-bottom: 20px;
&:hover {
border-color: var(--el-color-primary);
}
.el-icon.uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100%;
height: 120px;
text-align: center;
}
}
}
.img-list-box {
display: flex;
.img-item {
width: 120px;
position: relative;
margin-right: 10px;
.el-image {
width: 120px;
height: 120px;
border-radius: 5px;
}
.el-button {
position: absolute;
right: 5px;
top: 5px;
width: 20px;
height: 20px;
}
}
}
}
.el-row.text-info {
width: 100%;
padding: 10px 0;
.el-tag {
margin-right: 10px;
}
}
//
.submit-btn {
display: flex;
margin: 20px 0;
.el-button {
width: 200px;
}
}
.job-list-box {
//
@import 'running-job-list.styl';
.finish-job-list {
#waterfall {
display: flex;
justify-content: center;
padding-top: 20px;
flex-flow: column;
.waterfall-item {
overflow: visible;
.job-item {
width: 100%;
height: 100%;
border: 1px solid #666666;
padding: 6px;
border-radius: 6px;
// position relative
.el-image {
overflow: auto;
}
.opt {
padding-top: 5px;
.opt-line {
margin: 6px 0;
ul {
display: flex;
flex-flow: row;
li {
margin-right: 6px;
a {
padding: 3px 0;
width: 40px;
text-align: center;
border-radius: 5px;
display: block;
cursor: pointer;
background-color: #4E5058;
color: #fff;
&:hover {
background-color: #6D6F78;
}
}
}
.show-prompt {
font-size: 20px;
cursor: pointer;
}
}
}
}
.remove {
display: none;
position: absolute;
right: 10px;
top: 10px;
}
&:hover {
.remove {
display: block;
}
}
}
}
}
}
.el-image {
width: 100%;
height: 100%;
overflow: visible;
.el-image-viewer__wrapper {
img {
width: auto;
height: auto;
}
}
.image-slot {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
min-height: 220px;
color: var(--text-theme-color);
overflow: hidden;
.err-msg-container {
overflow: hidden;
word-break: break-all;
padding: 15px;
.title {
font-size: 20px;
text-align: center;
font-weight: bold;
color: #f56c6c;
margin-bottom: 30px;
}
.opt {
display: flex;
justify-content: center;
}
}
.iconfont {
font-size: 50px;
margin-bottom: 10px;
}
}
}
.el-image.upscale {
img {
// height 310px
}
.image-slot {
min-height: 310px;
}
.el-image-viewer__wrapper {
img {
width: auto;
height: auto;
}
}
}
}
.inner {
display: flex;
width: 100%;
.mj-box {
margin: 10px;
// background-color #262626
// border 1px solid #454545
// height: calc(100vh - 50px)
// overflow: scroll
min-width: 300px;
max-width: 300px;
padding: 20px;
border-radius: 10px;
color: var(--text-theme-color);
font-size: 14px;
overflow: auto;
h2 {
font-weight: bold;
font-size: 20px;
text-align: center;
color: var(--theme-textcolor-normal);
}
//
::-webkit-scrollbar {
width: 0;
height: 0;
background-color: transparent;
}
.mj-params {
margin-top: 10px;
overflow: auto;
.param-line {
padding: 0 10px;
.el-icon {
position: relative;
}
.model {
background: var(--card-bg);
// border 1px solid #454545
border-radius: 8px;
padding: 5px;
margin-bottom: 10px;
display: flex;
flex-flow: column;
align-items: center;
cursor: pointer;
border: 1px solid var(--chat-bg);
&:hover {
border: 1px solid var(--theme-border-hover);
}
.el-image {
height: 40px;
width: 100%;
}
.text {
margin-top: 4px;
font-size: 12px;
}
}
.model.active {
// color #47fff1
// background-color #585858
border: 1px solid var(--theme-border-hover);
}
.form-item-inner {
display: flex;
align-items: center;
.el-select {
--el-select-input-focus-border-color: var(--el-color-primary);
--el-input-focus-border-color: var(--el-color-primary);
}
.el-input__wrapper {
background: var(--chat-bg);
}
.el-input__inner {
color: var(--text-theme-color);
}
.el-icon {
margin-left: 10px;
}
}
.img-uploader {
.el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 100%;
transition: var(--el-transition-duration-fast);
&:hover {
border-color: var(--el-color-primary);
}
.el-icon.uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100%;
height: 120px;
text-align: center;
}
}
}
}
.param-line.pt {
display: flex;
align-items: center;
padding-top: 5px;
padding-bottom: 5px;
}
}
}
.el-form {
.el-form-item__label {
color: var(--text-theme-color);
}
.el-input, .el-slider {
width: 180px;
}
.uploader-icon {
font-size: 24px;
position: relative;
top: 3px;
}
}
.no-more-data {
text-align: center;
padding: 30px;
}
.generate-btn {
.iconfont {
margin-right: 5px;
}
}
}
}
.mj-list-item-prompt {
.el-icon {
margin-left: 10px;
cursor: pointer;
position: relative;
}
}

View File

@@ -0,0 +1,363 @@
.page-keling
display flex
min-height 100vh
:deep(.el-form-item__label)
color var(--text-theme-color)
.grid-content
// background-color #383838
background var(--card-bg)
border-radius 8px
padding 8px 14px
display flex
cursor pointer
margin-bottom 10px
// border 1px solid #383838
border 1px solid var(--chat-bg)
&:hover
border 1px solid var(--theme-border-hover)
.icon
width 20px
height 20px
margin-bottom 5px
.texts
margin-left 5px
margin-top 2px
color var(--text-theme-color)
.param-line.pt
padding-top 5px
padding-bottom 5px
.grid-content.active
// color #47fff1
// background-color #585858
border 1px solid var(--theme-border-hover)
.h-20
height 4rem !important
.main-content
padding-right 1.5rem
padding-left 1.5rem
padding-bottom 1rem
flex 1
background var(--chat-bg)
// width: 100%;
// padding 0 10px 10px 10px
color var(--text-theme-color)
overflow-x hidden
.camera-control
padding 10px
border-radius 4px
background var(--card-bg)
:deep(.el-form-item:last-child)
margin-bottom 0 !important
.title-tabs
:deep(.el-tabs__item.is-active)
color var(--theme-textcolor-normal)
font-size 18px
:deep(.el-tabs__item)
color var(--text-theme-color)
font-size 18px
.el-tabs
--el-tabs-header-height 55px
.el-tabs__item
color var(--text-theme-color)
font-size 18px
.el-tabs__item.is-active, .title-tabs .el-tabs__item.is-active
.title-tabs .el-tabs__active-bar
background-color var(--theme-textcolor-normal)
:deep(.el-textarea)
--el-input-focus-border-color var(--el-color-primary)
:deep(.el-textarea__inner)
background transparent
color var(--text-theme-color)
.el-input__wrapper
background transparent
padding 5px
.text
margin-bottom 10px
color #6b778c
font-size 15px
.param-line.pt
padding-top 5px
padding-bottom 5px
.form-item-inner
display flex
align-items center
.el-icon
margin-left 10px
.el-form-item__label
color var(--text-theme-color)
//
.img-inline
display flex
gap 20px
align-items center
.img-uploader
text-align center
:deep(.el-upload)
border 1px dashed var(--el-border-color)
border-radius 6px
cursor pointer
position relative
overflow hidden
width 120px
height 120px
line-height 120px
transition var(--el-transition-duration-fast)
margin-bottom 20px
&:hover
border-color var(--el-color-primary)
.el-icon.uploader-icon
font-size 28px
color #8c939d
width 100%
height 120px
text-align center
.img-list-box
display flex
.img-item
width 120px
position relative
margin-right 10px
.el-image
width 120px
height 120px
border-radius 5px
.el-button
position absolute
right 5px
top 5px
width 20px
height 20px
.el-row.text-info
width 100%
padding 10px 0
.el-tag
margin-right 10px
//
.submit-btn
display flex
margin 20px 0
.el-button
width 200px
.video-list
.btn
margin-right 10px
border none
border-radius 5px
padding 5px 10px
cursor pointer
color var(--theme-text-color-primary)
background-color var(--btn-bg)
&:hover
opacity 0.7
.list-box
padding 0
.item
display flex
flex-flow row
align-items center
min-height 100px
padding 10px 15px
border-radius 10px
cursor pointer
margin-bottom 20px
background var(--chat-bg)
.left
.container
width 160px
position relative
max-height 120px
overflow hidden
display flex
justify-content center
align-items center
.video
width 160px
border-radius 5px
.el-image
width 160px
height 90px
border-radius 5px
.duration
position absolute
bottom 0
right 0
background-color rgba(14, 8, 8, 0.7)
padding 0 3px
font-family 'Input Sans'
font-size 14px
font-weight 700
border-radius 0.125rem
.play
position absolute
width 100%
height 100%
top 0
left 50%
border none
border-radius 5px
background rgba(100, 100, 100, 0.3)
cursor pointer
color var(--text-theme-color)
opacity 0
transform translate(-50%, 0px)
transition opacity 0.3s ease 0s
&:hover
.play
opacity 1
// display block
.center
width 100%
// border 1px solid saddlebrown
display flex
justify-content center
align-items flex-start
flex-flow column
padding 0 20px
.prompt, .failed
padding 0
font-size 16px
max-height 60px
line-height 28px
overflow hidden
text-overflow ellipsis
.prompt
color var(--text-fb)
cursor text
.failed
color #E4696B
.right
display flex
justify-content right
min-width 200px
font-size 14px
padding 0
.tools
display flex
justify-content left
align-items center
flex-flow row
height 90px
.btn-publish
padding 2px 10px
.text
margin-right 10px
.btn-icon
background none
padding 6px
transition background 0.6s ease 0s
color #919191
&:hover
// background #5f5958
// color #e1e1e1
color var(--el-color-primary)
.downloading
width 16px
.pagination
margin-top 20px
display flex
justify-content center
.inner
display flex
width 100%
.mj-box
margin 10px
// background-color #262626
// border 1px solid #454545
// height: calc(100vh - 50px)
// overflow: scroll
min-width 300px
max-width 300px
padding 20px
border-radius 10px
color var(--text-theme-color)
font-size 14px
overflow auto
h2
font-weight bold
font-size 20px
text-align center
color var(--theme-textcolor-normal)
//
::-webkit-scrollbar
width 0
height 0
background-color transparent
.mj-params
margin-top 10px
overflow auto
.param-line
padding 0 10px
.el-icon
position relative
.model
background var(--card-bg)
// border 1px solid #454545
border-radius 8px
padding 5px
margin-bottom 10px
display flex
flex-flow column
align-items center
cursor pointer
border 1px solid var(--chat-bg)
&:hover
border 1px solid var(--theme-border-hover)
.el-image
height 40px
width 100%
.text
margin-top 4px
font-size 12px
.model.active
// color #47fff1
// background-color #585858
border 1px solid var(--theme-border-hover)
.form-item-inner
display flex
align-items center
.el-select
--el-select-input-focus-border-color var(--el-color-primary)
--el-input-focus-border-color var(--el-color-primary)
.el-input__wrapper
background var(--chat-bg)
.el-input__inner
color var(--text-theme-color)
.el-icon
margin-left 10px
.img-uploader
.el-upload
border 1px dashed var(--el-border-color)
border-radius 6px
cursor pointer
position relative
overflow hidden
width 100%
transition var(--el-transition-duration-fast)
&:hover
border-color var(--el-color-primary)
.el-icon.uploader-icon
font-size 28px
color #8c939d
width 100%
height 120px
text-align center
.param-line.pt
display flex
align-items center
padding-top 5px
padding-bottom 5px
.el-form
.el-form-item__label
color var(--text-theme-color)
.el-input, .el-slider
width 180px
.uploader-icon
font-size 24px
position relative
top 3px
.no-more-data
text-align center
padding 30px
.generate-btn
.iconfont
margin-right 5px

View File

@@ -2,6 +2,7 @@
display: flex;
justify-content: center;
background-color: #0E0808;
height: 100vh;
.inner {
text-align left
@@ -62,7 +63,6 @@
.prompt {
width 100%
height 500px
background-color transparent
white-space pre-wrap
overflow-y auto

View File

@@ -282,8 +282,8 @@
.model {
color #8f8f8f
// background-color #1C1616
// border 1px solid #8f8f8f
background-color var(--el-bg-color)
border 1px solid var(--el-border-color-light)
font-weight normal
font-size 12px
padding 1px 3px

View File

@@ -1,84 +1,99 @@
import {httpGet} from "@/utils/http";
import { httpGet } from "@/utils/http";
import Storage from "good-storage";
import {randString} from "@/utils/libs";
import { randString } from "@/utils/libs";
const userDataKey = "USER_INFO_CACHE_KEY"
const adminDataKey = "ADMIN_INFO_CACHE_KEY"
const systemInfoKey = "SYSTEM_INFO_CACHE_KEY"
const licenseInfoKey = "LICENSE_INFO_CACHE_KEY"
const userDataKey = "USER_INFO_CACHE_KEY";
const adminDataKey = "ADMIN_INFO_CACHE_KEY";
const systemInfoKey = "SYSTEM_INFO_CACHE_KEY";
const licenseInfoKey = "LICENSE_INFO_CACHE_KEY";
export function checkSession() {
return new Promise((resolve, reject) => {
httpGet('/api/user/session').then(res => {
resolve(res.data)
}).catch(e => {
Storage.remove(userDataKey)
reject(e)
})
})
const item = Storage.get(userDataKey) ?? { expire: 0, data: null };
if (item.expire > Date.now()) {
return Promise.resolve(item.data);
}
return new Promise((resolve, reject) => {
httpGet("/api/user/session")
.then((res) => {
item.data = res.data;
item.expire = Date.now() + 1000 * 3;
Storage.set(userDataKey, item);
resolve(item.data);
})
.catch((e) => {
Storage.remove(userDataKey);
reject(e);
});
});
}
export function checkAdminSession() {
const item = Storage.get(adminDataKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
return new Promise((resolve, reject) => {
httpGet('/api/admin/session').then(res => {
item.data = res.data
item.expire = Date.now() + 1000 * 30
Storage.set(adminDataKey, item)
resolve(item.data)
}).catch(e => {
Storage.remove(adminDataKey)
reject(e)
})
})
const item = Storage.get(adminDataKey) ?? { expire: 0, data: null };
if (item.expire > Date.now()) {
return Promise.resolve(item.data);
}
return new Promise((resolve, reject) => {
httpGet("/api/admin/session")
.then((res) => {
item.data = res.data;
item.expire = Date.now() + 1000 * 30;
Storage.set(adminDataKey, item);
resolve(item.data);
})
.catch((e) => {
Storage.remove(adminDataKey);
reject(e);
});
});
}
export function removeAdminInfo() {
Storage.remove(adminDataKey)
Storage.remove(adminDataKey);
}
export function getSystemInfo() {
const item = Storage.get(systemInfoKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
return new Promise((resolve, reject) => {
httpGet('/api/config/get?key=system').then(res => {
item.data = res
item.expire = Date.now() + 1000 * 30
Storage.set(systemInfoKey, item)
resolve(item.data)
}).catch(err => {
reject(err)
})
})
const item = Storage.get(systemInfoKey) ?? { expire: 0, data: null };
if (item.expire > Date.now()) {
return Promise.resolve(item.data);
}
return new Promise((resolve, reject) => {
httpGet("/api/config/get?key=system")
.then((res) => {
item.data = res;
item.expire = Date.now() + 1000 * 30;
Storage.set(systemInfoKey, item);
resolve(item.data);
})
.catch((err) => {
reject(err);
});
});
}
export function getLicenseInfo() {
const item = Storage.get(licenseInfoKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
const item = Storage.get(licenseInfoKey) ?? { expire: 0, data: null };
if (item.expire > Date.now()) {
return Promise.resolve(item.data);
}
return new Promise((resolve, reject) => {
httpGet('/api/config/license').then(res => {
item.data = res
item.expire = Date.now() + 1000 * 30
Storage.set(licenseInfoKey, item)
resolve(item.data)
}).catch(err => {
resolve(err)
})
})
return new Promise((resolve, reject) => {
httpGet("/api/config/license")
.then((res) => {
item.data = res;
item.expire = Date.now() + 1000 * 30;
Storage.set(licenseInfoKey, item);
resolve(item.data);
})
.catch((err) => {
resolve(err);
});
});
}
export function getClientId() {
let clientId = Storage.get('client_id')
if (clientId) {
return clientId
}
clientId = randString(42)
Storage.set('client_id', clientId)
return clientId
}
let clientId = Storage.get("client_id");
if (clientId) {
return clientId;
}
clientId = randString(42);
Storage.set("client_id", clientId);
return clientId;
}

File diff suppressed because it is too large Load Diff

View File

@@ -58,7 +58,7 @@
<img src="/images/play.svg" alt="" />
</button>
</div>
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress > 100" />
<el-image :src="item.cover_url" fit="cover" v-else-if="item.progress === 101" />
<generating message="正在生成视频" v-else />
</div>
</div>
@@ -68,10 +68,16 @@
</div>
<div class="right" v-if="item.progress === 100">
<div class="tools">
<button class="btn btn-publish">
<!-- <button class="btn btn-publish">
<span class="text">发布</span>
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
</button>
</button> -->
<el-tooltip content="复制提示词" placement="top">
<button class="btn btn-icon copy-prompt" :data-clipboard-text="item.prompt">
<i class="iconfont icon-copy"></i>
</button>
</el-tooltip>
<el-tooltip content="下载视频" placement="top">
<button class="btn btn-icon" @click="download(item)" :disabled="item.downloading">
@@ -111,7 +117,7 @@
</div>
</el-container>
<black-dialog v-model:show="showDialog" title="预览视频" hide-footer @cancal="showDialog = false" width="auto">
<video style="width: 100%; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
<video style="max-width: 90vw; max-height: 90vh" :src="currentVideoUrl" preload="auto" :autoplay="true" loop="loop" muted="muted" v-show="showDialog">
您的浏览器不支持视频播放
</video>
</black-dialog>
@@ -131,7 +137,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import Generating from "@/components/ui/Generating.vue";
import BlackDialog from "@/components/ui/BlackDialog.vue";
import Clipboard from "clipboard";
const showDialog = ref(false);
const currentVideoUrl = ref("");
const row = ref(1);
@@ -152,16 +158,31 @@ const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const taskPulling = ref(true);
const clipboard = ref(null);
const pullHandler = ref(null);
onMounted(() => {
checkSession().then(() => {
fetchData(1);
setInterval(() => {
// 设置轮询
pullHandler.value = setInterval(() => {
if (taskPulling.value) {
fetchData(1);
}
}, 5000);
});
clipboard.value = new Clipboard(".copy-prompt");
clipboard.value.on("success", () => {
ElMessage.success("复制成功!");
});
});
onUnmounted(() => {
clipboard.value.destroy();
if (pullHandler.value) {
clearInterval(pullHandler.value);
}
});
const download = (item) => {

View File

@@ -1,5 +1,5 @@
<template>
<div class="page-song" :style="{ height: winHeight + 'px' }">
<div class="page-song">
<div class="inner">
<h2 class="title">{{ song.title }}</h2>
<div class="row tags" v-if="song.tags">
@@ -11,15 +11,10 @@
<el-avatar :size="32" :src="song.user?.avatar" />
</span>
<span class="nickname">{{ song.user?.nickname }}</span>
<button class="btn btn-icon" @click="play">
<i class="iconfont icon-play"></i> {{ song.play_times }}
</button>
<button class="btn btn-icon" @click="play"><i class="iconfont icon-play"></i> {{ song.play_times }}</button>
<el-tooltip content="复制歌曲链接" placement="top">
<button
class="btn btn-icon copy-link"
:data-clipboard-text="getShareURL(song)"
>
<button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(song)">
<i class="iconfont icon-share1"></i>
</button>
</el-tooltip>
@@ -31,18 +26,12 @@
</div>
<div class="row">
<textarea class="prompt" maxlength="2000" rows="18" readonly>{{
song.prompt
}}</textarea>
<textarea class="prompt" maxlength="2000" rows="18" readonly>{{ song.prompt }}</textarea>
</div>
</div>
<div class="music-player" v-if="playList.length > 0">
<music-player
:songs="playList"
ref="playerRef"
@play="song.play_times += 1"
/>
<music-player :songs="playList" ref="playerRef" @play="song.play_times += 1" />
</div>
</div>
</template>
@@ -67,8 +56,7 @@ httpGet("/api/suno/detail", { song_id: id })
.then((res) => {
song.value = res.data;
playList.value = [song.value];
document.title =
song.value?.title + " | By " + song.value?.user.nickname + " | Suno音乐";
document.title = song.value?.title + " | By " + song.value?.user.nickname + " | Suno音乐";
})
.catch((e) => {
showMessageError("获取歌曲详情失败:" + e.message);