mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-02-15 10:54:24 +08:00
merge v4.1.1 and fixed conflicts
This commit is contained in:
@@ -15,18 +15,13 @@
|
||||
<div class="info-text">{{ scope.item.hello_msg }}</div>
|
||||
</div>
|
||||
<div class="btn">
|
||||
<div v-if="hasRole(scope.item.key)">
|
||||
<el-button size="small" color="#21aa93" @click="useRole(scope.item)">使用</el-button>
|
||||
<el-button size="small" color="#21aa93" @click="useRole(scope.item)">使用</el-button>
|
||||
<el-tooltip effect="light" content="从工作区移除" placement="top" v-if="hasRole(scope.item.key)">
|
||||
<el-button size="small" type="danger" @click="updateRole(scope.item,'remove')">移除</el-button>
|
||||
</div>
|
||||
<el-button v-else size="small"
|
||||
style="--el-color-primary:#009999"
|
||||
@click="updateRole(scope.item, 'add')">
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
<span>添加应用</span>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" content="添加到工作区" placement="top" v-else>
|
||||
<el-button size="small" style="--el-color-primary:#009999" @click="updateRole(scope.item, 'add')">添加</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +72,7 @@ const roles = ref([])
|
||||
const store = useSharedStore();
|
||||
|
||||
onMounted(() => {
|
||||
httpGet("/api/role/list?all=true").then((res) => {
|
||||
httpGet("/api/role/list").then((res) => {
|
||||
const items = res.data
|
||||
// 处理 hello message
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
@@ -118,6 +118,8 @@
|
||||
v-if="item.type==='prompt'" :data="item" :list-style="listStyle"/>
|
||||
<chat-reply v-else-if="item.type==='reply'" :data="item" @regen="reGenerate" :read-only="false" :list-style="listStyle"/>
|
||||
</div>
|
||||
|
||||
<back-top :right="30" :bottom="100" bg-color="#19C27D"/>
|
||||
</div><!-- end chat box -->
|
||||
|
||||
<div class="input-box">
|
||||
@@ -219,6 +221,9 @@ import {useSharedStore} from "@/store/sharedata";
|
||||
import FileSelect from "@/components/FileSelect.vue";
|
||||
import FileList from "@/components/FileList.vue";
|
||||
import ChatSetting from "@/components/ChatSetting.vue";
|
||||
import axios from "axios";
|
||||
import BackTop from "@/components/BackTop.vue";
|
||||
import {showMessageError} from "@/utils/dialog";
|
||||
|
||||
const title = ref('ChatGPT-智能助手');
|
||||
const models = ref([])
|
||||
@@ -316,18 +321,17 @@ const initData = () => {
|
||||
chatList.value = res.data;
|
||||
allChats.value = res.data;
|
||||
}
|
||||
|
||||
if (router.currentRoute.value.query.role_id) {
|
||||
roleId.value = parseInt(router.currentRoute.value.query.role_id)
|
||||
}
|
||||
// 加载模型
|
||||
httpGet('/api/model/list').then(res => {
|
||||
models.value = res.data
|
||||
modelID.value = models.value[0].id
|
||||
|
||||
// 加载角色列表
|
||||
httpGet(`/api/role/list`).then((res) => {
|
||||
httpGet(`/api/role/list`,{id:roleId.value}).then((res) => {
|
||||
roles.value = res.data;
|
||||
if (router.currentRoute.value.query.role_id) {
|
||||
roleId.value = parseInt(router.currentRoute.value.query.role_id)
|
||||
} else {
|
||||
if (!roleId.value) {
|
||||
roleId.value = roles.value[0]['id']
|
||||
}
|
||||
|
||||
@@ -343,18 +347,8 @@ const initData = () => {
|
||||
})
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
// 加载会话
|
||||
httpGet("/api/chat/list").then((res) => {
|
||||
if (res.data) {
|
||||
chatList.value = res.data;
|
||||
allChats.value = res.data;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessage.error("加载会话列表失败!")
|
||||
})
|
||||
|
||||
// 加载模型
|
||||
httpGet('/api/model/list').then(res => {
|
||||
httpGet('/api/model/list',{id:roleId.value}).then(res => {
|
||||
models.value = res.data
|
||||
modelID.value = models.value[0].id
|
||||
}).catch(e => {
|
||||
@@ -369,6 +363,37 @@ const initData = () => {
|
||||
ElMessage.error('获取聊天角色失败: ' + e.messages)
|
||||
})
|
||||
})
|
||||
|
||||
inputRef.value.addEventListener('paste', (event) => {
|
||||
const items = (event.clipboardData || window.clipboardData).items;
|
||||
let fileFound = false;
|
||||
loading.value = true
|
||||
|
||||
for (let item of items) {
|
||||
if (item.kind === 'file') {
|
||||
const file = item.getAsFile();
|
||||
fileFound = true;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
// 执行上传操作
|
||||
httpPost('/api/upload', formData).then((res) => {
|
||||
files.value.push(res.data)
|
||||
ElMessage.success({message: "上传成功", duration: 500})
|
||||
loading.value = false
|
||||
}).catch((e) => {
|
||||
ElMessage.error('文件上传失败:' + e.message)
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fileFound) {
|
||||
document.getElementById('status').innerText = 'No file found in paste data.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getRoleById = function (rid) {
|
||||
@@ -582,18 +607,6 @@ const connect = function (chat_id, role_id) {
|
||||
}
|
||||
}
|
||||
|
||||
// 心跳函数
|
||||
const sendHeartbeat = () => {
|
||||
clearTimeout(heartbeatHandle.value)
|
||||
new Promise((resolve, reject) => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
|
||||
}
|
||||
resolve("success")
|
||||
}).then(() => {
|
||||
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
|
||||
});
|
||||
}
|
||||
const _socket = new WebSocket(host + `/api/chat/new?session_id=${sessionId.value}&role_id=${role_id}&chat_id=${chat_id}&model_id=${modelID.value}&token=${getUserToken()}`);
|
||||
_socket.addEventListener('open', () => {
|
||||
chatData.value = []; // 初始化聊天数据
|
||||
@@ -615,8 +628,6 @@ const connect = function (chat_id, role_id) {
|
||||
} else { // 加载聊天记录
|
||||
loadChatHistory(chat_id);
|
||||
}
|
||||
// 发送心跳消息
|
||||
sendHeartbeat()
|
||||
});
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
@@ -627,13 +638,14 @@ const connect = function (chat_id, role_id) {
|
||||
reader.onload = () => {
|
||||
const data = JSON.parse(String(reader.result));
|
||||
if (data.type === 'start') {
|
||||
const prePrompt = chatData.value[chatData.value.length-1].content
|
||||
const prePrompt = chatData.value[chatData.value.length-1]?.content
|
||||
chatData.value.push({
|
||||
type: "reply",
|
||||
id: randString(32),
|
||||
icon: _role['icon'],
|
||||
prompt:prePrompt,
|
||||
content: ""
|
||||
content: "",
|
||||
orgContent: "",
|
||||
});
|
||||
} else if (data.type === 'end') { // 消息接收完毕
|
||||
// 追加当前会话到会话列表
|
||||
@@ -667,8 +679,10 @@ const connect = function (chat_id, role_id) {
|
||||
} else {
|
||||
lineBuffer.value += data.content;
|
||||
const reply = chatData.value[chatData.value.length - 1]
|
||||
reply['orgContent'] = lineBuffer.value;
|
||||
reply['content'] = md.render(processContent(lineBuffer.value));
|
||||
if (reply) {
|
||||
reply['orgContent'] = lineBuffer.value;
|
||||
reply['content'] = md.render(processContent(lineBuffer.value));
|
||||
}
|
||||
}
|
||||
// 将聊天框的滚动条滑动到最底部
|
||||
nextTick(() => {
|
||||
@@ -678,7 +692,7 @@ const connect = function (chat_id, role_id) {
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.warn(e)
|
||||
}
|
||||
|
||||
});
|
||||
@@ -694,7 +708,7 @@ const connect = function (chat_id, role_id) {
|
||||
connect(chat_id, role_id)
|
||||
}).catch(() => {
|
||||
loading.value = true
|
||||
setTimeout(() => connect(chat_id, role_id), 3000)
|
||||
showMessageError("会话已断开,刷新页面...")
|
||||
});
|
||||
});
|
||||
|
||||
@@ -740,7 +754,7 @@ const onInput = (e) => {
|
||||
const autofillPrompt = (text) => {
|
||||
prompt.value = text
|
||||
inputRef.value.focus()
|
||||
// sendMessage()
|
||||
sendMessage()
|
||||
}
|
||||
// 发送消息
|
||||
const sendMessage = function () {
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
</div> <!-- end finish job list-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
|
||||
</div><!-- end task list box -->
|
||||
</div>
|
||||
|
||||
@@ -193,6 +193,7 @@ import Clipboard from "clipboard";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
import BackTop from "@/components/BackTop.vue";
|
||||
|
||||
const listBoxHeight = ref(0)
|
||||
// const paramBoxHeight = ref(0)
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
|
||||
<div class="navbar">
|
||||
<el-tooltip
|
||||
v-if="!licenseConfig.de_copy"
|
||||
v-if="!license.de_copy"
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="部署文档"
|
||||
placement="bottom">
|
||||
<a href="https://ai.r9it.com/docs/install/" class="link-button" target="_blank">
|
||||
<a href="https://docs.geekai.me/install/" class="link-button" target="_blank">
|
||||
<i class="iconfont icon-book"></i>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip
|
||||
v-if="!licenseConfig.de_copy"
|
||||
v-if="!license.de_copy"
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="项目源码"
|
||||
@@ -46,7 +46,7 @@
|
||||
<span class="username">{{ loginUser.nickname }}</span>
|
||||
</el-dropdown-item>
|
||||
|
||||
<div v-if="!licenseConfig.de_copy">
|
||||
<div v-if="!license.de_copy">
|
||||
<el-dropdown-item>
|
||||
<i class="iconfont icon-book"></i>
|
||||
<a :href="docsURL" target="_blank">
|
||||
@@ -146,7 +146,7 @@ import ConfigDialog from "@/components/UserInfoDialog.vue";
|
||||
import {showMessageError} from "@/utils/dialog";
|
||||
|
||||
const router = useRouter();
|
||||
const logo = ref('/images/logo.png');
|
||||
const logo = ref('');
|
||||
const mainNavs = ref([])
|
||||
const moreNavs = ref([])
|
||||
const curPath = ref(router.currentRoute.value.path)
|
||||
@@ -156,7 +156,7 @@ const loginUser = ref({})
|
||||
const version = ref(process.env.VUE_APP_VERSION)
|
||||
const routerViewKey = ref(0)
|
||||
const showConfigDialog = ref(false)
|
||||
const licenseConfig = ref({})
|
||||
const license = ref({de_copy: true})
|
||||
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
|
||||
const gitURL = ref(process.env.VUE_APP_GIT_URL)
|
||||
|
||||
@@ -166,6 +166,12 @@ watch(() => store.showLoginDialog, (newValue) => {
|
||||
show.value = newValue
|
||||
});
|
||||
|
||||
// 监听路由变化
|
||||
router.beforeEach((to, from, next) => {
|
||||
curPath.value = to.path
|
||||
next();
|
||||
});
|
||||
|
||||
if (curPath.value === "/external") {
|
||||
curPath.value = router.currentRoute.value.query.url
|
||||
}
|
||||
@@ -199,8 +205,9 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
httpGet("/api/config/license").then(res => {
|
||||
licenseConfig.value = res.data
|
||||
license.value = res.data
|
||||
}).catch(e => {
|
||||
license.value = {de_copy: false}
|
||||
showMessageError("获取 License 配置:" + e.message)
|
||||
})
|
||||
|
||||
|
||||
@@ -487,6 +487,23 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-image v-else-if="slotProp.item['err_msg'] !== ''">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<div class="err-msg-container">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
|
||||
<template #reference>
|
||||
<el-button type="info">详情</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-image v-else>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
@@ -536,16 +553,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle/>
|
||||
<el-button type="warning" v-if="slotProp.item.publish"
|
||||
@click="publishImage(slotProp.item, false)"
|
||||
circle>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage(slotProp.item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<div class="remove" v-if="slotProp.item.progress === 100">
|
||||
<el-tooltip effect="light" content="删除任务" placement="top">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle/>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" content="取消发布" placement="top" v-if="slotProp.item.publish">
|
||||
<el-button type="warning"
|
||||
@click="publishImage(slotProp.item, false)"
|
||||
circle>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" content="发布图片" placement="top" v-else>
|
||||
<el-button type="success" @click="publishImage(slotProp.item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip effect="light" content="复制提示词" placement="top">
|
||||
<el-button type="success" class="copy-prompt-mj"
|
||||
:data-clipboard-text="slotProp.item.prompt" circle>
|
||||
<el-icon><DocumentCopy/></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -562,6 +593,7 @@
|
||||
</div> <!-- end finish job list-->
|
||||
</div>
|
||||
</div>
|
||||
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
|
||||
</div><!-- end task list box -->
|
||||
</div>
|
||||
|
||||
@@ -582,6 +614,7 @@ import {getSessionId} from "@/store/session";
|
||||
import {copyObj, removeArrayItem} from "@/utils/libs";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
import BackTop from "@/components/BackTop.vue";
|
||||
|
||||
const listBoxHeight = ref(0)
|
||||
const paramBoxHeight = ref(0)
|
||||
@@ -714,7 +747,7 @@ const connect = () => {
|
||||
reader.readAsText(event.data, "UTF-8")
|
||||
reader.onload = () => {
|
||||
const message = String(reader.result)
|
||||
if (message === "FINISH") {
|
||||
if (message === "FINISH" || message === "FAIL") {
|
||||
page.value = 0
|
||||
isOver.value = false
|
||||
fetchFinishJobs(page.value)
|
||||
@@ -786,7 +819,7 @@ const fetchRunningJobs = () => {
|
||||
const jobs = res.data
|
||||
const _jobs = []
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
if (jobs[i].progress === 101) {
|
||||
ElNotification({
|
||||
title: '任务执行失败',
|
||||
dangerouslyUseHTMLString: true,
|
||||
@@ -799,7 +832,6 @@ const fetchRunningJobs = () => {
|
||||
} else {
|
||||
power.value += mjActionPower.value
|
||||
}
|
||||
continue
|
||||
}
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
@@ -833,7 +865,7 @@ const fetchFinishJobs = () => {
|
||||
jobs[i]['thumb_url'] = '/images/img-placeholder.jpg'
|
||||
}
|
||||
|
||||
if (jobs[i].type === 'image' || jobs[i].type === 'variation') {
|
||||
if ((jobs[i].type === 'image' || jobs[i].type === 'variation') && jobs[i].progress === 100) {
|
||||
jobs[i]['can_opt'] = true
|
||||
}
|
||||
}
|
||||
@@ -984,7 +1016,7 @@ const publishImage = (item, action) => {
|
||||
item.publish = action
|
||||
page.value = 0
|
||||
isOver.value = false
|
||||
fetchFinishJobs()
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
ElMessage.error(text + "失败:" + e.message)
|
||||
})
|
||||
|
||||
@@ -345,7 +345,7 @@
|
||||
</div> <!-- end finish job list-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
|
||||
</div><!-- end task list box -->
|
||||
</div>
|
||||
|
||||
@@ -476,6 +476,7 @@ import {useRouter} from "vue-router";
|
||||
import {getSessionId} from "@/store/session";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
import BackTop from "@/components/BackTop.vue";
|
||||
|
||||
const listBoxHeight = ref(0)
|
||||
// const paramBoxHeight = ref(0)
|
||||
@@ -755,7 +756,7 @@ const publishImage = (event, item, action) => {
|
||||
item.publish = action
|
||||
page.value = 0
|
||||
isOver.value = false
|
||||
fetchFinishJobs()
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
ElMessage.error(text + "失败:" + e.message)
|
||||
})
|
||||
|
||||
@@ -163,7 +163,10 @@
|
||||
<i class="iconfont icon-face"></i>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
|
||||
|
||||
</div><!-- end of waterfall -->
|
||||
|
||||
</div>
|
||||
<!-- 任务详情弹框 -->
|
||||
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
|
||||
@@ -301,6 +304,7 @@ import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import Clipboard from "clipboard";
|
||||
import {useRouter} from "vue-router";
|
||||
import BackTop from "@/components/BackTop.vue";
|
||||
|
||||
const data = ref({
|
||||
"mj": [],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="index-page" :style="{height: winHeight+'px'}">
|
||||
<div class="index-bg" :style="{backgroundImage: 'url('+bgImgUrl+')'}"></div>
|
||||
<div :class="theme.imageBg?'color-bg image-bg':'color-bg'" :style="{backgroundImage:'url('+bgStyle.backgroundImage+')', backgroundColor:bgStyle.backgroundColor}"></div>
|
||||
<div class="menu-box">
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
@@ -8,19 +8,19 @@
|
||||
>
|
||||
<div class="menu-item">
|
||||
<el-image :src="logo" alt="Geek-AI"/>
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="title" :style="{color:theme.textColor}">{{ title }}</div>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<span v-if="!licenseConfig.de_copy">
|
||||
<span v-if="!license.de_copy">
|
||||
<a :href="docsURL" target="_blank">
|
||||
<el-button type="primary" round>
|
||||
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" round>
|
||||
<i class="iconfont icon-book"></i>
|
||||
<span>文档</span>
|
||||
</el-button>
|
||||
</a>
|
||||
|
||||
<a :href="gitURL" target="_blank">
|
||||
<el-button type="success" round>
|
||||
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" round>
|
||||
<i class="iconfont icon-github"></i>
|
||||
<span>源码</span>
|
||||
</el-button>
|
||||
@@ -28,38 +28,29 @@
|
||||
</span>
|
||||
|
||||
<span v-if="!isLogin">
|
||||
<el-button @click="router.push('/login')" round>登录</el-button>
|
||||
<el-button @click="router.push('/register')" round>注册</el-button>
|
||||
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" @click="router.push('/login')" class="shadow" round>登录</el-button>
|
||||
<el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" @click="router.push('/register')" class="shadow" round>注册</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h1>欢迎使用 {{ title }}</h1>
|
||||
<p>{{ slogan }}</p>
|
||||
<el-button @click="router.push('/chat')" color="#ffffff" style="color:#007bff" :dark="false">
|
||||
<i class="iconfont icon-chat"></i>
|
||||
<span>AI 对话</span>
|
||||
</el-button>
|
||||
<el-button @click="router.push('/mj')" color="#C4CCFD" style="color:#424282" :dark="false">
|
||||
<i class="iconfont icon-mj"></i>
|
||||
<span>MJ 绘画</span>
|
||||
</el-button>
|
||||
<h1 :style="{color:theme.textColor}">欢迎使用 {{ title }}</h1>
|
||||
<p :style="{color:theme.textColor}">{{ slogan }}</p>
|
||||
|
||||
<el-button @click="router.push('/sd')" color="#4AE6DF" style="color:#424282" :dark="false">
|
||||
<i class="iconfont icon-sd"></i>
|
||||
<span>SD 绘画</span>
|
||||
</el-button>
|
||||
<el-button @click="router.push('/xmind')" color="#FFFD55" style="color:#424282" :dark="false">
|
||||
<i class="iconfont icon-xmind"></i>
|
||||
<span>思维导图</span>
|
||||
</el-button>
|
||||
<!-- <div id="animation-container"></div>-->
|
||||
<div class="navs">
|
||||
<el-space wrap>
|
||||
<div v-for="item in navs" class="nav-item">
|
||||
<el-button @click="router.push(item.url)" :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" :dark="false">
|
||||
<i :class="'iconfont '+iconMap[item.url]"></i>
|
||||
<span>{{item.name}}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer" v-if="!licenseConfig.de_copy">
|
||||
<footer-bar />
|
||||
</div>
|
||||
<footer-bar :text-color="theme.textColor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -79,36 +70,102 @@ if (isMobile()) {
|
||||
router.push("/mobile")
|
||||
}
|
||||
|
||||
const title = ref("Geek-AI 创作系统")
|
||||
const logo = ref("/images/logo.png")
|
||||
const slogan = ref("我辈之人,先干为敬,陪您先把 AI 用起来")
|
||||
const licenseConfig = ref({})
|
||||
const title = ref("")
|
||||
const logo = ref("")
|
||||
const slogan = ref("")
|
||||
const license = ref({de_copy: true})
|
||||
const winHeight = window.innerHeight - 150
|
||||
const bgImgUrl = ref('')
|
||||
const isLogin = ref(false)
|
||||
const docsURL = ref(process.env.VUE_APP_DOCS_URL)
|
||||
const gitURL = ref(process.env.VUE_APP_GIT_URL)
|
||||
const navs = ref([])
|
||||
const btnColors = ref([
|
||||
{bgColor: "#fff143", textColor: "#50616D"},
|
||||
{bgColor: "#eaff56", textColor: "#50616D"},
|
||||
{bgColor: "#bddd22", textColor: "#50616D"},
|
||||
{bgColor: "#1bd1a5", textColor: "#50616D"},
|
||||
{bgColor: "#e0eee8", textColor: "#50616D"},
|
||||
{bgColor: "#7bcfa6", textColor: "#50616D"},
|
||||
{bgColor: "#bce672", textColor: "#50616D"},
|
||||
{bgColor: "#44cef6", textColor: "#ffffff"},
|
||||
{bgColor: "#70f3ff", textColor: "#50616D"},
|
||||
{bgColor: "#fffbf0", textColor: "#50616D"},
|
||||
{bgColor: "#d6ecf0", textColor: "#50616D"},
|
||||
{bgColor: "#88ada6", textColor: "#50616D"},
|
||||
{bgColor: "#30dff3", textColor: "#50616D"},
|
||||
{bgColor: "#d3e0f3", textColor: "#50616D"},
|
||||
{bgColor: "#e9e7ef", textColor: "#50616D"},
|
||||
{bgColor: "#eacd76", textColor: "#50616D"},
|
||||
{bgColor: "#f2be45", textColor: "#50616D"},
|
||||
{bgColor: "#549688", textColor: "#ffffff"},
|
||||
{bgColor: "#758a99", textColor: "#ffffff"},
|
||||
{bgColor: "#41555d", textColor: "#ffffff"},
|
||||
{bgColor: "#21aa93", textColor: "#ffffff"},
|
||||
{bgColor: "#0aa344", textColor: "#ffffff"},
|
||||
{bgColor: "#f05654", textColor: "#ffffff"},
|
||||
{bgColor: "#db5a6b", textColor: "#ffffff"},
|
||||
{bgColor: "#db5a6b", textColor: "#ffffff"},
|
||||
{bgColor: "#8d4bbb", textColor: "#ffffff"},
|
||||
{bgColor: "#426666", textColor: "#ffffff"},
|
||||
{bgColor: "#177cb0", textColor: "#ffffff"},
|
||||
{bgColor: "#395260", textColor: "#ffffff"},
|
||||
{bgColor: "#519a73", textColor: "#ffffff"},
|
||||
{bgColor: "#75878a", textColor: "#ffffff"},
|
||||
])
|
||||
const iconMap =ref(
|
||||
{
|
||||
"/chat": "icon-chat",
|
||||
"/mj": "icon-mj",
|
||||
"/sd": "icon-sd",
|
||||
"/dalle": "icon-dalle",
|
||||
"/images-wall": "icon-image",
|
||||
"/suno": "icon-suno",
|
||||
"/xmind": "icon-xmind",
|
||||
"/apps": "icon-app",
|
||||
"/member": "icon-vip-user",
|
||||
"/invite": "icon-share",
|
||||
}
|
||||
)
|
||||
const bgStyle = {}
|
||||
const color = btnColors.value[Math.floor(Math.random() * btnColors.value.length)]
|
||||
const theme = ref({bgColor: "#ffffff", btnBgColor: color.bgColor, btnTextColor: color.textColor, textColor: "#ffffff", imageBg:true})
|
||||
|
||||
onMounted(() => {
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
title.value = res.data.title
|
||||
logo.value = res.data.logo
|
||||
if (res.data.index_bg_url) {
|
||||
bgImgUrl.value = res.data.index_bg_url
|
||||
if (res.data.index_bg_url === 'color') {
|
||||
// 随机选取一种颜色
|
||||
theme.value.bgColor = color.bgColor
|
||||
theme.value.btnBgColor = color.bgColor
|
||||
theme.value.textColor = color.textColor
|
||||
theme.value.btnTextColor = color.textColor
|
||||
// 设置背景颜色
|
||||
bgStyle.backgroundColor = theme.value.bgColor
|
||||
bgStyle.backgroundImage = "/images/transparent-bg.png"
|
||||
theme.value.imageBg = false
|
||||
} else if (res.data.index_bg_url) {
|
||||
bgStyle.backgroundImage = res.data.index_bg_url
|
||||
} else {
|
||||
bgImgUrl.value = "/images/index-bg.jpg"
|
||||
}
|
||||
if (res.data.slogan) {
|
||||
slogan.value = res.data.slogan
|
||||
bgStyle.backgroundImage = "/images/index-bg.jpg"
|
||||
}
|
||||
|
||||
slogan.value = res.data.slogan
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
|
||||
httpGet("/api/config/license").then(res => {
|
||||
licenseConfig.value = res.data
|
||||
license.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取 License 配置:" + e.message)
|
||||
license.value = {de_copy: false}
|
||||
ElMessage.error("获取 License 配置失败:" + e.message)
|
||||
})
|
||||
|
||||
httpGet("/api/menu/list?index=1").then(res => {
|
||||
navs.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取导航菜单失败:" + e.message)
|
||||
})
|
||||
|
||||
checkSession().then(() => {
|
||||
@@ -119,107 +176,5 @@ onMounted(() => {
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '@/assets/iconfont/iconfont.css'
|
||||
.index-page {
|
||||
margin: 0
|
||||
overflow hidden
|
||||
color #ffffff
|
||||
display flex
|
||||
justify-content center
|
||||
align-items baseline
|
||||
padding-top 150px
|
||||
|
||||
.index-bg {
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 100vw
|
||||
height 100vh
|
||||
filter: blur(8px);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.menu-box {
|
||||
position absolute
|
||||
top 0
|
||||
width 100%
|
||||
display flex
|
||||
|
||||
.el-menu {
|
||||
padding 0 30px
|
||||
width 100%
|
||||
display flex
|
||||
justify-content space-between
|
||||
background none
|
||||
border none
|
||||
|
||||
.menu-item {
|
||||
display flex
|
||||
padding 20px 0
|
||||
|
||||
color #ffffff
|
||||
|
||||
.title {
|
||||
font-size 24px
|
||||
padding 10px 10px 0 10px
|
||||
}
|
||||
|
||||
.el-image {
|
||||
height 50px
|
||||
}
|
||||
|
||||
.el-button {
|
||||
margin-left 10px
|
||||
|
||||
span {
|
||||
margin-left 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
position relative
|
||||
|
||||
h1 {
|
||||
font-size: 5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
padding: 25px 20px;
|
||||
font-size: 1.3rem;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.iconfont {
|
||||
font-size 1.6rem
|
||||
margin-right 10px
|
||||
}
|
||||
}
|
||||
|
||||
#animation-container {
|
||||
display flex
|
||||
justify-content center
|
||||
width 100%
|
||||
height: 300px;
|
||||
position: absolute;
|
||||
top: 350px
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
.el-link__inner {
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@import "@/assets/css/index.styl"
|
||||
</style>
|
||||
|
||||
@@ -55,10 +55,8 @@
|
||||
</div>
|
||||
|
||||
<reset-pass @hide="showResetPass = false" :show="showResetPass"/>
|
||||
|
||||
<footer class="footer" v-if="!licenseConfig.de_copy">
|
||||
<footer-bar/>
|
||||
</footer>
|
||||
|
||||
<footer-bar/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -81,7 +79,7 @@ const title = ref('Geek-AI');
|
||||
const username = ref(process.env.VUE_APP_USER);
|
||||
const password = ref(process.env.VUE_APP_PASS);
|
||||
const showResetPass = ref(false)
|
||||
const logo = ref("/images/logo.png")
|
||||
const logo = ref("")
|
||||
const licenseConfig = ref({})
|
||||
const wechatLoginURL = ref('')
|
||||
|
||||
|
||||
@@ -106,19 +106,10 @@ import {useSharedStore} from "@/store/sharedata";
|
||||
|
||||
const leftBoxHeight = ref(window.innerHeight - 105)
|
||||
const rightBoxHeight = ref(window.innerHeight - 115)
|
||||
const title = ref("")
|
||||
|
||||
const prompt = ref("")
|
||||
const text = ref(`# Geek-AI 助手
|
||||
|
||||
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||||
- 基于 Websocket 实现,完美的打字机体验。
|
||||
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
|
||||
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||||
- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
||||
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||||
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
|
||||
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。
|
||||
`)
|
||||
const text = ref("")
|
||||
const md = require('markdown-it')({breaks: true});
|
||||
const content = ref(text.value)
|
||||
const html = ref("")
|
||||
@@ -135,7 +126,20 @@ const models = ref([])
|
||||
const modelID = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
title.value = res.data.title??process.env.VUE_APP_TITLE
|
||||
text.value = `# ${title.value}
|
||||
|
||||
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||||
- 基于 Websocket 实现,完美的打字机体验。
|
||||
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
|
||||
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||||
- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
||||
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||||
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
|
||||
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。
|
||||
`
|
||||
content.value = text.value
|
||||
initData()
|
||||
try {
|
||||
markMap.value = Markmap.create(svgRef.value)
|
||||
@@ -145,7 +149,9 @@ onMounted(() => {
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
});
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
httpGet("/api/model/list").then(res => {
|
||||
@@ -333,7 +339,6 @@ const downloadImage = () => {
|
||||
a.download = "geek-ai-xmind.png"
|
||||
a.href = canvas.toDataURL(`image/png`)
|
||||
a.click()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,8 +183,8 @@ import {validateEmail, validateMobile} from "@/utils/validate";
|
||||
import {showMessageError, showMessageOK} from "@/utils/dialog";
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref('Geek-AI 用户注册');
|
||||
const logo = ref("/images/logo")
|
||||
const title = ref('');
|
||||
const logo = ref("")
|
||||
const data = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
@@ -196,7 +196,7 @@ const data = ref({
|
||||
const enableMobile = ref(false)
|
||||
const enableEmail = ref(false)
|
||||
const enableUser = ref(false)
|
||||
const enableRegister = ref(false)
|
||||
const enableRegister = ref(true)
|
||||
const activeName = ref("mobile")
|
||||
const wxImg = ref("/images/wx.png")
|
||||
const licenseConfig = ref({})
|
||||
|
||||
95
web/src/views/Song.vue
Normal file
95
web/src/views/Song.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="page-song" :style="{ height: winHeight + 'px' }">
|
||||
<div class="inner">
|
||||
<h2 class="title">{{song.title}}</h2>
|
||||
<div class="row tags" v-if="song.tags">
|
||||
<span>{{song.tags}}</span>
|
||||
</div>
|
||||
|
||||
<div class="row author">
|
||||
<span>
|
||||
<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>
|
||||
|
||||
<el-tooltip effect="light" content="复制歌曲链接" placement="top">
|
||||
<button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(song)" >
|
||||
<i class="iconfont icon-share1"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="row date">
|
||||
<span>{{dateFormat(song.created_at)}}</span>
|
||||
<span class="version">{{song.raw_data?.major_model_version}}</span>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<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"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, onUnmounted, ref} from "vue"
|
||||
import {useRouter} from "vue-router";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {showMessageError} from "@/utils/dialog";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import Clipboard from "clipboard";
|
||||
import {ElMessage} from "element-plus";
|
||||
import MusicPlayer from "@/components/MusicPlayer.vue";
|
||||
|
||||
const router = useRouter()
|
||||
const id = router.currentRoute.value.params.id
|
||||
const song = ref({title:""})
|
||||
const playList = ref([])
|
||||
const playerRef = ref(null)
|
||||
|
||||
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音乐"
|
||||
}).catch(e => {
|
||||
showMessageError("获取歌曲详情失败:"+e.message)
|
||||
})
|
||||
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
clipboard.value = new Clipboard('.copy-link');
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success("复制歌曲链接成功!");
|
||||
})
|
||||
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
})
|
||||
|
||||
// 播放歌曲
|
||||
const play = () => {
|
||||
playerRef.value.play()
|
||||
}
|
||||
|
||||
|
||||
const winHeight = ref(window.innerHeight-50)
|
||||
const getShareURL = (item) => {
|
||||
return `${location.protocol}//${location.host}/song/${item.id}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/song.styl"
|
||||
</style>
|
||||
576
web/src/views/Suno.vue
Normal file
576
web/src/views/Suno.vue
Normal file
@@ -0,0 +1,576 @@
|
||||
<template>
|
||||
<div class="page-suno" :style="{ height: winHeight + 'px' }">
|
||||
<div class="left-bar">
|
||||
<div class="bar-top">
|
||||
<el-tooltip effect="light" content="定义模式" placement="top">
|
||||
<black-switch v-model:value="custom" size="large" />
|
||||
</el-tooltip>
|
||||
<black-select v-model:value="data.model" :options="models" placeholder="请选择模型" style="width: 100px" />
|
||||
</div>
|
||||
|
||||
<div class="params">
|
||||
<div class="pure-music">
|
||||
<span class="switch"><black-switch v-model:value="data.instrumental" size="default" /></span>
|
||||
<span class="text">纯音乐</span>
|
||||
</div>
|
||||
<div v-if="custom">
|
||||
<div class="item-group" v-if="!data.instrumental">
|
||||
<div class="label">
|
||||
<span class="text">歌词</span>
|
||||
<el-popover placement="right"
|
||||
:width="200"
|
||||
trigger="hover" content="自己写歌词或寻求 AI 的帮助。使用两节歌词(8 行)可获得最佳效果。">
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="item"
|
||||
v-loading="generating"
|
||||
element-loading-text="正在生成歌词..."
|
||||
element-loading-background="rgba(122, 122, 122, 0.8)">
|
||||
<black-input v-model:value="data.lyrics" type="textarea" :rows="10" placeholder="请在这里输入你自己写的歌词..."/>
|
||||
<button class="btn btn-lyric" @click="createLyric">生成歌词</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<div class="label">
|
||||
<span class="text">音乐风格</span>
|
||||
<el-popover placement="right"
|
||||
:width="200"
|
||||
trigger="hover" content="描述您想要的音乐风格(例如“原声流行音乐”)。Sunos 模特无法识别艺术家的名字,但能够理解音乐流派和氛围。">
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="item">
|
||||
<black-input v-model:value="data.tags" type="textarea" :maxlength="120" :rows="3" placeholder="请输入音乐风格,多个风格之间用英文逗号隔开..."/>
|
||||
</div>
|
||||
|
||||
<div class="tag-select">
|
||||
<div class="inner">
|
||||
<span
|
||||
class="tag"
|
||||
@click="selectTag(tag)"
|
||||
v-for="tag in tags"
|
||||
:key="tag.value">{{ tag.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-group">
|
||||
<div class="label">
|
||||
<span class="text">歌曲名称</span>
|
||||
<el-popover placement="right"
|
||||
:width="200"
|
||||
trigger="hover" content="给你的歌曲起一个标题,以便于分享、发现和组织。">
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="item">
|
||||
<black-input v-model:value="data.title" type="textarea" :rows="1" placeholder="请输入歌曲名称..."/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="label">
|
||||
<span class="text">歌曲描述</span>
|
||||
<el-popover placement="right"
|
||||
:width="200"
|
||||
trigger="hover" content="描述您想要的音乐风格和主题(例如:关于假期的流行音乐)。请使用流派和氛围,而不是特定的艺术家和歌曲风格,AI无法识别。">
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<div class="item">
|
||||
<black-input v-model:value="data.prompt" type="textarea" :rows="10" placeholder="例如:一首关于鸟人的摇滚歌曲..."/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ref-song" v-if="refSong">
|
||||
<div class="label">
|
||||
<span class="text">续写</span>
|
||||
<el-popover placement="right"
|
||||
:width="200"
|
||||
trigger="hover" content="输入额外的歌词,根据您之前的歌词来扩展歌曲。">
|
||||
<template #reference>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<div class="song">
|
||||
<el-image :src="refSong.cover_url" fit="cover" />
|
||||
<span class="title">{{refSong.title}}</span>
|
||||
<el-button type="info" @click="removeRefSong" size="small" :icon="Delete" circle />
|
||||
</div>
|
||||
<div class="extend-secs">
|
||||
从 <input v-model="refSong.extend_secs" type="text"/> 秒开始续写
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item">
|
||||
<button class="create-btn" @click="create">
|
||||
<img src="/images/create-new.svg" alt=""/>
|
||||
<span>{{btnText}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-box" v-loading="loading" element-loading-background="rgba(100,100,100,0.3)">
|
||||
<div class="list-box" v-if="!noData">
|
||||
<div v-for="item in list">
|
||||
<div class="item" v-if="item.progress === 100">
|
||||
<div class="left">
|
||||
<div class="container">
|
||||
<el-image :src="item.cover_url" fit="cover" />
|
||||
<div class="duration">{{formatTime(item.duration)}}</div>
|
||||
<button class="play" @click="play(item)">
|
||||
<img src="/images/play.svg" alt=""/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<div class="title">
|
||||
<a :href="'/song/'+item.song_id" target="_blank">{{item.title}}</a>
|
||||
<span class="model">{{item.major_model_version}}</span>
|
||||
<span class="model" v-if="item.ref_song">
|
||||
<i class="iconfont icon-link"></i>
|
||||
{{item.ref_song.title}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tags">{{item.tags}}</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="tools">
|
||||
<el-tooltip effect="light" content="以当前歌曲为素材继续创作" placement="top">
|
||||
<button class="btn" @click="extend(item)">续写</button>
|
||||
</el-tooltip>
|
||||
|
||||
<button class="btn btn-publish">
|
||||
<span class="text">发布</span>
|
||||
<black-switch v-model:value="item.publish" @change="publishJob(item)" size="small" />
|
||||
</button>
|
||||
|
||||
<el-tooltip effect="light" content="下载歌曲" placement="top">
|
||||
<a :href="item.audio_url" :download="item.title+'.mp3'" target="_blank">
|
||||
<button class="btn btn-icon">
|
||||
<i class="iconfont icon-download"></i>
|
||||
</button>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip effect="light" content="复制歌曲链接" placement="top">
|
||||
<button class="btn btn-icon copy-link" :data-clipboard-text="getShareURL(item)" >
|
||||
<i class="iconfont icon-share1"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip effect="light" content="编辑" placement="top">
|
||||
<button class="btn btn-icon" @click="update(item)">
|
||||
<i class="iconfont icon-edit"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip effect="light" content="删除" placement="top">
|
||||
<button class="btn btn-icon" @click="removeJob(item)">
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task" v-else>
|
||||
<div class="left">
|
||||
<div class="title">
|
||||
<span v-if="item.title">{{item.title}}</span>
|
||||
<span v-else>{{item.prompt}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
{{item.err_msg}}
|
||||
</div>
|
||||
<generating v-else />
|
||||
</div>
|
||||
<div class="right">
|
||||
<el-button type="info" @click="removeJob(item)" circle>
|
||||
<i class="iconfont icon-remove"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" description="没有任何作品,赶紧去创作吧!" v-else/>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination v-if="total > pageSize" background
|
||||
style="--el-pagination-button-bg-color:#414141;
|
||||
--el-pagination-button-color:#d1d1d1;
|
||||
--el-disabled-bg-color:#414141;
|
||||
--el-color-primary:#666666;
|
||||
--el-pagination-hover-color:#e1e1e1"
|
||||
layout="total,prev, pager, next"
|
||||
:hide-on-single-page="true"
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
@current-change="fetchData(page)"
|
||||
:total="total"/>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="music-player" v-if="showPlayer">
|
||||
<music-player :songs="playList" ref="playerRef" :show-close="true" @close="showPlayer = false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<black-dialog v-model:show="showDialog" title="修改歌曲" @cancal="showDialog = false" @confirm="updateSong" :width="500">
|
||||
<form class="form">
|
||||
<div class="form-item">
|
||||
<div class="label">歌曲名称</div>
|
||||
<input class="input" v-model="editData.title" type="text" />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<div class="label">封面图片</div>
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadCover"
|
||||
accept=".png,.jpg,.jpeg,.bmp"
|
||||
>
|
||||
<el-avatar :src="editData.cover" shape="square" :size="100"/>
|
||||
</el-upload>
|
||||
</div>
|
||||
</form>
|
||||
</black-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue"
|
||||
import {Delete, InfoFilled} from "@element-plus/icons-vue";
|
||||
import BlackSelect from "@/components/ui/BlackSelect.vue";
|
||||
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
|
||||
import BlackInput from "@/components/ui/BlackInput.vue";
|
||||
import MusicPlayer from "@/components/MusicPlayer.vue";
|
||||
import {compact} from "lodash";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {showMessageError, showMessageOK} from "@/utils/dialog";
|
||||
import Generating from "@/components/ui/Generating.vue";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import {formatTime} from "@/utils/libs";
|
||||
import Clipboard from "clipboard";
|
||||
import BlackDialog from "@/components/ui/BlackDialog.vue";
|
||||
import Compressor from "compressorjs";
|
||||
|
||||
const winHeight = ref(window.innerHeight - 50)
|
||||
const custom = ref(false)
|
||||
const models = ref([
|
||||
{label: "v3.0", value: "chirp-v3-0"},
|
||||
{label: "v3.5", value:"chirp-v3-5"}
|
||||
])
|
||||
const tags = ref([
|
||||
{label: "女声", value: "female vocals"},
|
||||
{label: "男声", value: "male vocals"},
|
||||
{label: "流行", value: "pop"},
|
||||
{label: "摇滚", value: "rock"},
|
||||
{label: "硬摇滚", value: "hard rock"},
|
||||
{label: "电音", value: "electronic"},
|
||||
{label: "金属", value: "metal"},
|
||||
{label: "重金属", value: "heavy metal"},
|
||||
{label: "节拍", value: "beat"},
|
||||
{label: "弱拍", value: "upbeat"},
|
||||
{label: "合成器", value: "synth"},
|
||||
{label: "吉他", value: "guitar"},
|
||||
{label: "钢琴", value: "piano"},
|
||||
{label: "小提琴", value: "violin"},
|
||||
{label: "贝斯", value: "bass"},
|
||||
{label: "嘻哈", value: "hip hop"},
|
||||
])
|
||||
const data = ref({
|
||||
model: "chirp-v3-0",
|
||||
tags: "",
|
||||
lyrics: "",
|
||||
prompt: "",
|
||||
title: "",
|
||||
instrumental: false,
|
||||
ref_task_id: "",
|
||||
extend_secs: 0,
|
||||
ref_song_id: "",
|
||||
})
|
||||
const loading = ref(true)
|
||||
const noData = ref(false)
|
||||
const playList = ref([])
|
||||
const playerRef = ref(null)
|
||||
const showPlayer = ref(false)
|
||||
const list = ref([])
|
||||
const btnText = ref("开始创作")
|
||||
const refSong = ref(null)
|
||||
const showDialog = ref(false)
|
||||
const editData = ref({title:"",cover:"",id:0})
|
||||
|
||||
const socket = ref(null)
|
||||
const userId = ref(0)
|
||||
const connect = () => {
|
||||
let host = process.env.VUE_APP_WS_HOST
|
||||
if (host === '') {
|
||||
if (location.protocol === 'https:') {
|
||||
host = 'wss://' + location.host;
|
||||
} else {
|
||||
host = 'ws://' + location.host;
|
||||
}
|
||||
}
|
||||
|
||||
const _socket = new WebSocket(host + `/api/suno/client?user_id=${userId.value}`);
|
||||
_socket.addEventListener('open', () => {
|
||||
socket.value = _socket;
|
||||
});
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8")
|
||||
reader.onload = () => {
|
||||
const message = String(reader.result)
|
||||
console.log(message)
|
||||
if (message === "FINISH" || message === "FAIL") {
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_socket.addEventListener('close', () => {
|
||||
if (socket.value !== null) {
|
||||
connect()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
clipboard.value = new Clipboard('.copy-link');
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success("复制歌曲链接成功!");
|
||||
})
|
||||
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
|
||||
checkSession().then(user => {
|
||||
userId.value = user.id
|
||||
fetchData(1)
|
||||
connect()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
})
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const total = ref(0)
|
||||
const fetchData = (_page) => {
|
||||
if (_page) {
|
||||
page.value = _page
|
||||
}
|
||||
httpGet("/api/suno/list",{page:page.value, page_size:pageSize.value}).then(res => {
|
||||
total.value = res.data.total
|
||||
const items = []
|
||||
for (let v of res.data.items) {
|
||||
if (v.progress === 100) {
|
||||
v.major_model_version = v['raw_data']['major_model_version']
|
||||
}
|
||||
items.push(v)
|
||||
}
|
||||
loading.value = false
|
||||
list.value = items
|
||||
noData.value = list.value.length === 0
|
||||
}).catch(e => {
|
||||
showMessageError("获取作品列表失败:"+e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 创建新的歌曲
|
||||
const create = () => {
|
||||
data.value.type = custom.value ? 2 : 1
|
||||
data.value.ref_task_id = refSong.value ? refSong.value.task_id : ""
|
||||
data.value.ref_song_id = refSong.value ? refSong.value.song_id : ""
|
||||
data.value.extend_secs = refSong.value ? refSong.value.extend_secs : 0
|
||||
if (custom.value) {
|
||||
if (data.value.lyrics === "") {
|
||||
return showMessageError("请输入歌词")
|
||||
}
|
||||
if (data.value.title === "") {
|
||||
return showMessageError("请输入歌曲标题")
|
||||
}
|
||||
} else {
|
||||
if (data.value.prompt === "") {
|
||||
return showMessageError("请输入歌曲描述")
|
||||
}
|
||||
}
|
||||
if (refSong.value && data.value.extend_secs > refSong.value.duration) {
|
||||
return showMessageError("续写开始时间不能超过原歌曲长度")
|
||||
}
|
||||
|
||||
httpPost("/api/suno/create", data.value).then(() => {
|
||||
fetchData(1)
|
||||
showMessageOK("创建任务成功")
|
||||
}).catch(e => {
|
||||
showMessageError("创建任务失败:"+e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 续写歌曲
|
||||
const extend = (item) => {
|
||||
refSong.value = item
|
||||
refSong.value.extend_secs = item.duration
|
||||
data.value.title = item.title
|
||||
custom.value = true
|
||||
btnText.value = "续写歌曲"
|
||||
}
|
||||
|
||||
// 更细歌曲
|
||||
const update = (item) => {
|
||||
showDialog.value = true
|
||||
editData.value.title = item.title
|
||||
editData.value.cover = item.cover_url
|
||||
editData.value.id = item.id
|
||||
}
|
||||
|
||||
const updateSong = () => {
|
||||
if (editData.value.title === "" || editData.value.cover === "") {
|
||||
return showMessageError("歌曲标题和封面不能为空")
|
||||
}
|
||||
httpPost("/api/suno/update", editData.value).then(() => {
|
||||
showMessageOK("更新歌曲成功")
|
||||
showDialog.value = false
|
||||
fetchData()
|
||||
}).catch(e => {
|
||||
showMessageError("更新歌曲失败:"+e.message)
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => custom.value, (newValue) => {
|
||||
if (!newValue) {
|
||||
removeRefSong()
|
||||
}
|
||||
})
|
||||
|
||||
const removeRefSong = () => {
|
||||
refSong.value = null
|
||||
btnText.value = "开始创作"
|
||||
}
|
||||
|
||||
const play = (item) => {
|
||||
playList.value = [item]
|
||||
showPlayer.value = true
|
||||
nextTick(()=> playerRef.value.play())
|
||||
}
|
||||
|
||||
const selectTag = (tag) => {
|
||||
if (data.value.tags.length + tag.value.length >= 119) {
|
||||
return
|
||||
}
|
||||
data.value.tags = compact([...data.value.tags.split(","), tag.value]).join(",")
|
||||
}
|
||||
|
||||
const removeJob = (item) => {
|
||||
ElMessageBox.confirm(
|
||||
'此操作将会删除任务相关文件,继续操作码?',
|
||||
'删除提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
httpGet("/api/suno/remove", {id: item.id}).then(() => {
|
||||
ElMessage.success("任务删除成功")
|
||||
fetchData()
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务删除失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const publishJob = (item) => {
|
||||
httpGet("/api/suno/publish", {id: item.id, publish:item.publish}).then(() => {
|
||||
ElMessage.success("操作成功")
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const getShareURL = (item) => {
|
||||
return `${location.protocol}//${location.host}/song/${item.id}`
|
||||
}
|
||||
|
||||
const uploadCover = (file) => {
|
||||
// 压缩图片并上传
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', result, result.name);
|
||||
// 执行上传操作
|
||||
httpPost('/api/upload', formData).then((res) => {
|
||||
editData.value.cover = res.data.url
|
||||
ElMessage.success({message: "上传成功", duration: 500})
|
||||
}).catch((e) => {
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
})
|
||||
},
|
||||
error(err) {
|
||||
console.log(err.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const generating = ref(false)
|
||||
const createLyric = () => {
|
||||
if (data.value.lyrics === "") {
|
||||
return showMessageError("请输入歌词描述")
|
||||
}
|
||||
generating.value = true
|
||||
httpPost("/api/suno/lyric", {prompt: data.value.lyrics}).then(res => {
|
||||
const lines = res.data.split('\n');
|
||||
data.value.title = lines.shift().replace(/\*/g,"")
|
||||
lines.shift()
|
||||
data.value.lyrics = lines.join('\n');
|
||||
generating.value = false
|
||||
}).catch(e => {
|
||||
showMessageError("歌词生成失败:"+e.message)
|
||||
generating.value = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/suno.styl"
|
||||
</style>
|
||||
@@ -2,19 +2,11 @@
|
||||
<div class="container list" v-loading="loading">
|
||||
|
||||
<div class="handle-box">
|
||||
<el-select v-model="query.platform" placeholder="平台" class="handle-input">
|
||||
<el-option
|
||||
v-for="item in platforms"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select v-model="query.type" placeholder="类型" class="handle-input">
|
||||
<el-option
|
||||
v-for="item in types"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -28,7 +20,6 @@
|
||||
|
||||
<el-row>
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
|
||||
<el-table-column prop="platform" label="所属平台"/>
|
||||
<el-table-column prop="name" label="名称"/>
|
||||
<el-table-column prop="value" label="API KEY">
|
||||
<template #default="scope">
|
||||
@@ -46,10 +37,9 @@
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="用途">
|
||||
<el-table-column prop="type" label="类型">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.type === 'chat'">聊天</el-tag>
|
||||
<el-tag v-else-if="scope.row.type === 'img'" type="success">绘图</el-tag>
|
||||
{{getTypeName(scope.row.type)}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="proxy_url" label="代理地址"/>
|
||||
@@ -84,30 +74,14 @@
|
||||
:close-on-click-modal="false"
|
||||
:title="title"
|
||||
>
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 10px; font-size:14px;">
|
||||
<p><b>注意:</b>如果是百度文心一言平台,API-KEY 为 APIKey|SecretKey,中间用竖线(|)连接</p>
|
||||
<p><b>注意:</b>如果是讯飞星火大模型,API-KEY 为 AppId|APIKey|APISecret,中间用竖线(|)连接</p>
|
||||
</el-alert>
|
||||
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||
<el-form-item label="所属平台:" prop="platform">
|
||||
<el-select v-model="item.platform" placeholder="请选择平台" @change="changePlatform">
|
||||
<el-option v-for="item in platforms" :value="item.value" :label="item.name" :key="item.value">{{
|
||||
item.name
|
||||
}}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称:" prop="name">
|
||||
<el-input v-model="item.name" autocomplete="off"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="用途:" prop="type">
|
||||
<el-select v-model="item.type" placeholder="请选择用途" @change="changeType">
|
||||
<el-option v-for="item in types" :value="item.value" :label="item.name" :key="item.value">{{
|
||||
item.name
|
||||
<el-form-item label="类型:" prop="type">
|
||||
<el-select v-model="item.type" placeholder="请选择类型">
|
||||
<el-option v-for="item in types" :value="item.value" :label="item.label" :key="item.value">{{
|
||||
item.label
|
||||
}}
|
||||
</el-option>
|
||||
</el-select>
|
||||
@@ -117,12 +91,12 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="API URL:" prop="api_url">
|
||||
<el-input v-model="item.api_url" autocomplete="off"
|
||||
placeholder="必须填土完整的 Chat API URL,如:https://api.openai.com/v1/chat/completions"/>
|
||||
<div class="info">如果你使用了第三方中转,这里就填写中转地址</div>
|
||||
placeholder="只填 BASE URL 即可,如:https://api.openai.com"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="代理地址:" prop="proxy_url">
|
||||
<el-input v-model="item.proxy_url" autocomplete="off"/>
|
||||
<div class="info">如果想要通过代理来访问 API,请填写代理地址,如:http://127.0.0.1:7890</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态:" prop="enable">
|
||||
@@ -150,22 +124,24 @@ import ClipboardJS from "clipboard";
|
||||
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
const query = ref({type: '',platform:''})
|
||||
const query = ref({type: ''})
|
||||
const item = ref({})
|
||||
const showDialog = ref(false)
|
||||
const rules = reactive({
|
||||
platform: [{required: true, message: '请选择平台', trigger: 'change',}],
|
||||
name: [{required: true, message: '请输入名称', trigger: 'change',}],
|
||||
type: [{required: true, message: '请选择用途', trigger: 'change',}],
|
||||
value: [{required: true, message: '请输入 API KEY 值', trigger: 'change',}]
|
||||
})
|
||||
|
||||
const loading = ref(true)
|
||||
const formRef = ref(null)
|
||||
const title = ref("")
|
||||
const platforms = ref([])
|
||||
const types = ref([
|
||||
{name: "聊天", value: "chat"},
|
||||
{name: "绘画", value: "img"},
|
||||
{label: "对话", value:"chat"},
|
||||
{label: "Midjourney", value:"mj"},
|
||||
{label: "DALL-E", value:"dalle"},
|
||||
{label: "Suno文生歌", value:"suno"},
|
||||
{label: "Luma视频", value:"luma"},
|
||||
])
|
||||
|
||||
|
||||
@@ -180,12 +156,6 @@ onMounted(() => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
|
||||
httpGet("/api/admin/config/get/app").then(res => {
|
||||
platforms.value = res.data.platforms
|
||||
}).catch(e =>{
|
||||
ElMessage.error("获取配置失败:"+e.message)
|
||||
})
|
||||
|
||||
fetchData()
|
||||
})
|
||||
|
||||
@@ -193,6 +163,15 @@ onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
})
|
||||
|
||||
const getTypeName = (type) => {
|
||||
for (let v of types.value) {
|
||||
if (v.value === type) {
|
||||
return v.label
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
|
||||
const fetchData = () => {
|
||||
@@ -262,26 +241,6 @@ const set = (filed, row) => {
|
||||
})
|
||||
}
|
||||
|
||||
const selectedPlatform = ref(null)
|
||||
const changePlatform = (value) => {
|
||||
console.log(value)
|
||||
for (let v of platforms.value) {
|
||||
if (v.value === value) {
|
||||
selectedPlatform.value = v
|
||||
item.value.api_url = v.chat_url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const changeType = (value) => {
|
||||
if (selectedPlatform.value) {
|
||||
if(value === 'img') {
|
||||
item.value.api_url = selectedPlatform.value.img_url
|
||||
} else {
|
||||
item.value.api_url = selectedPlatform.value.chat_url
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
@@ -2,14 +2,7 @@
|
||||
<div class="container model-list" v-loading="loading">
|
||||
|
||||
<div class="handle-box">
|
||||
<el-select v-model="query.platform" placeholder="平台" class="handle-input">
|
||||
<el-option
|
||||
v-for="item in platforms"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-model="query.name" placeholder="模型名称" class="handle-input" />
|
||||
|
||||
<el-button :icon="Search" @click="fetchData">搜索</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
||||
@@ -17,11 +10,6 @@
|
||||
|
||||
<el-row>
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto">
|
||||
<el-table-column prop="platform" label="所属平台">
|
||||
<template #default="scope">
|
||||
<span class="sort" :data-id="scope.row.id">{{ scope.row.platform }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="模型名称"/>
|
||||
<el-table-column prop="value" label="模型值">
|
||||
<template #default="scope">
|
||||
@@ -46,11 +34,6 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- <el-table-column label="创建时间">-->
|
||||
<!-- <template #default="scope">-->
|
||||
<!-- <span>{{ dateFormat(scope.row['created_at']) }}</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
<el-table-column prop="key_name" label="绑定API-KEY"/>
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
@@ -72,15 +55,6 @@
|
||||
style="width: 90%; max-width: 600px;"
|
||||
>
|
||||
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
|
||||
<el-form-item label="所属平台:" prop="platform">
|
||||
<el-select v-model="item.platform" placeholder="请选择平台">
|
||||
<el-option v-for="item in platforms" :value="item.value" :label="item.name" :key="item.value">{{
|
||||
item.name
|
||||
}}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模型名称:" prop="name">
|
||||
<el-input v-model="item.name" autocomplete="off"/>
|
||||
</el-form-item>
|
||||
@@ -89,7 +63,7 @@
|
||||
<el-input v-model="item.value" autocomplete="off"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="费率:" prop="weight">
|
||||
<el-form-item label="消耗算力:" prop="weight">
|
||||
<template #default>
|
||||
<div class="tip-input">
|
||||
<el-input-number :min="0" v-model="item.power" autocomplete="off"/>
|
||||
@@ -121,18 +95,7 @@
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
raw-content
|
||||
content="gpt-3.5-turbo:4096 <br/>
|
||||
gpt-3.5-turbo-16k: 16384 <br/>
|
||||
gpt-4: 8192 <br/>
|
||||
gpt-4-32k: 32768 <br/>
|
||||
chatglm_pro: 32768 <br/>
|
||||
chatglm_std: 16384 <br/>
|
||||
chatglm_lite: 4096 <br/>
|
||||
qwen-turbo: 8192 <br/>
|
||||
qwen-plus: 32768 <br/>
|
||||
文心一言: 8192 <br/>
|
||||
星火1.0: 4096 <br/>
|
||||
星火2.0-星火3.5: 8192"
|
||||
content="去各大模型的官方 API 文档查询模型支持的最大上下文长度"
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
@@ -214,18 +177,16 @@ import ClipboardJS from "clipboard";
|
||||
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
const query = ref({platform:''})
|
||||
const query = ref({name:''})
|
||||
const item = ref({})
|
||||
const showDialog = ref(false)
|
||||
const title = ref("")
|
||||
const rules = reactive({
|
||||
platform: [{required: true, message: '请选择平台', trigger: 'change',}],
|
||||
name: [{required: true, message: '请输入模型名称', trigger: 'change',}],
|
||||
value: [{required: true, message: '请输入模型值', trigger: 'change',}]
|
||||
})
|
||||
const loading = ref(true)
|
||||
const formRef = ref(null)
|
||||
const platforms = ref([])
|
||||
|
||||
// 获取 API KEY
|
||||
const apiKeys = ref([])
|
||||
@@ -290,12 +251,6 @@ onMounted(() => {
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
|
||||
httpGet("/api/admin/config/get/app").then(res => {
|
||||
platforms.value = res.data.platforms
|
||||
}).catch(e =>{
|
||||
ElMessage.error("获取配置失败:"+e.message)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -305,7 +260,7 @@ onUnmounted(() => {
|
||||
const add = function () {
|
||||
title.value = "新增模型"
|
||||
showDialog.value = true
|
||||
item.value = {enabled: true, weight: 1, open: true}
|
||||
item.value = {enabled: true, power: 1, open: true,max_tokens: 1024,max_context: 8192, temperature: 0.9,}
|
||||
}
|
||||
|
||||
const edit = function (row) {
|
||||
|
||||
@@ -59,7 +59,7 @@ const router = useRouter();
|
||||
const title = ref('Geek-AI Console');
|
||||
const username = ref(process.env.VUE_APP_ADMIN_USER);
|
||||
const password = ref(process.env.VUE_APP_ADMIN_PASS);
|
||||
const logo = ref("/images/logo.png")
|
||||
const logo = ref("")
|
||||
|
||||
checkAdminSession().then(() => {
|
||||
router.push("/admin")
|
||||
|
||||
@@ -50,9 +50,45 @@
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="system.index_bg_url = 'https://api.dujin.org/bing/1920.php'">使用动态背景</el-button>
|
||||
<el-button @click="system.index_bg_url = 'color'">使用纯色背景</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="首页导航菜单" prop="index_navs">
|
||||
<div class="tip-input">
|
||||
<el-select
|
||||
v-model="system['index_navs']"
|
||||
multiple
|
||||
:filterable="true"
|
||||
placeholder="请选择菜单,多选"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in menus"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="info">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="被选中的菜单将会在首页导航栏显示"
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="版权信息" prop="copyright">
|
||||
<el-input v-model="system['copyright']" placeholder="更改此选项需要获取 License 授权"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="开放注册" prop="enabled_register">
|
||||
<div class="tip-input">
|
||||
<el-switch v-model="system['enabled_register']"/>
|
||||
@@ -218,6 +254,9 @@
|
||||
<el-form-item label="DALL-E-3算力" prop="dall_power">
|
||||
<el-input v-model.number="system['dall_power']" placeholder="使用DALL-E-3画一张图消耗算力"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Suno 算力" prop="suno_power">
|
||||
<el-input v-model.number="system['suno_power']" placeholder="使用 Suno 生成一首音乐消耗算力"/>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="众筹支付">
|
||||
<el-form-item label="启用众筹功能" prop="enabled_reward">
|
||||
@@ -391,22 +430,25 @@ import {InfoFilled, UploadFilled,Select,CloseBold} from "@element-plus/icons-vue
|
||||
import MdEditor from "md-editor-v3";
|
||||
import 'md-editor-v3/lib/style.css';
|
||||
import Menu from "@/views/admin/Menu.vue";
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {copyObj, dateFormat} from "@/utils/libs";
|
||||
import AIDrawing from "@/views/admin/AIDrawing.vue";
|
||||
|
||||
const activeName = ref('basic')
|
||||
const system = ref({models: []})
|
||||
const configBak = ref({})
|
||||
const loading = ref(true)
|
||||
const systemFormRef = ref(null)
|
||||
const models = ref([])
|
||||
const openAIModels = ref([])
|
||||
const notice = ref("")
|
||||
const license = ref({is_active: false})
|
||||
const menus = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
// 加载系统配置
|
||||
httpGet('/api/admin/config/get?key=system').then(res => {
|
||||
system.value = res.data
|
||||
configBak.value = copyObj(system.value)
|
||||
}).catch(e => {
|
||||
ElMessage.error("加载系统配置失败: " + e.message)
|
||||
})
|
||||
@@ -425,6 +467,12 @@ onMounted(() => {
|
||||
ElMessage.error("获取模型失败:" + e.message)
|
||||
})
|
||||
|
||||
httpGet('/api/admin/menu/list').then(res => {
|
||||
menus.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取模型失败:" + e.message)
|
||||
})
|
||||
|
||||
fetchLicense()
|
||||
})
|
||||
|
||||
@@ -447,7 +495,7 @@ const save = function (key) {
|
||||
systemFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
system.value['power_price'] = parseFloat(system.value['power_price']) ?? 0
|
||||
httpPost('/api/admin/config/update', {key: key, config: system.value}).then(() => {
|
||||
httpPost('/api/admin/config/update', {key: key, config: system.value, config_bak: configBak.value}).then(() => {
|
||||
ElMessage.success("操作成功!")
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message)
|
||||
|
||||
@@ -105,7 +105,7 @@ checkSession().then((user) => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
// 加载角色列表
|
||||
httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
|
||||
httpGet(`/api/role/list`).then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
@@ -109,7 +109,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const fetchApps = () => {
|
||||
httpGet("/api/role/list?all=true").then((res) => {
|
||||
httpGet("/api/role/list").then((res) => {
|
||||
const items = res.data
|
||||
// 处理 hello message
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<div class="content">
|
||||
<van-form>
|
||||
<div class="avatar">
|
||||
<van-uploader v-model="fileList"
|
||||
reupload max-count="1"
|
||||
:deletable="false"
|
||||
:after-read="afterRead"/>
|
||||
<van-image :src="fileList[0].url" size="80" width="80" fit="cover" round />
|
||||
<!-- <van-uploader v-model="fileList"-->
|
||||
<!-- reupload max-count="1"-->
|
||||
<!-- :deletable="false"-->
|
||||
<!-- :after-read="afterRead"/>-->
|
||||
</div>
|
||||
<van-cell-group inset v-model="form">
|
||||
<van-field
|
||||
@@ -154,7 +155,7 @@
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {showFailToast, showNotify, showSuccessToast, showToast} from "vant";
|
||||
import {showFailToast, showNotify, showSuccessToast} from "vant";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import Compressor from 'compressorjs';
|
||||
import {dateFormat, isWeChatBrowser, showLoginDialog} from "@/utils/libs";
|
||||
|
||||
@@ -407,8 +407,16 @@ const connect = () => {
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs(1)
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8")
|
||||
reader.onload = () => {
|
||||
const message = String(reader.result)
|
||||
if (message === "FINISH" || message === "FAIL") {
|
||||
page.value = 1
|
||||
fetchFinishJobs(1)
|
||||
}
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -456,7 +464,7 @@ const fetchFinishJobs = (page) => {
|
||||
httpGet(`/api/mj/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
|
||||
const jobs = res.data
|
||||
for (let i = 0; i < jobs.length; i++) {
|
||||
if (jobs[i].progress === -1) {
|
||||
if (jobs[i].progress === 101) {
|
||||
showNotify({
|
||||
message: `任务ID:${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
|
||||
type: 'danger',
|
||||
@@ -479,7 +487,7 @@ const fetchFinishJobs = (page) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (jobs[i].type === 'image' || jobs[i].type === 'variation') {
|
||||
if ((jobs[i].type === 'image' || jobs[i].type === 'variation') && jobs[i].progress === 100){
|
||||
jobs[i]['can_opt'] = true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user