merge v4.1.1 and fixed conflicts

This commit is contained in:
RockYang
2024-11-13 18:40:04 +08:00
110 changed files with 4820 additions and 1977 deletions

View File

@@ -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++) {

View File

@@ -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 () {

View File

@@ -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)

View File

@@ -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)
})

View File

@@ -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)
})

View File

@@ -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)
})

View File

@@ -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": [],

View File

@@ -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>

View File

@@ -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('')

View File

@@ -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 实现,完美的打字机体验。
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
- 支持 OPenAIAzure文心一言讯飞星火清华 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 实现,完美的打字机体验。
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
- 支持 OPenAIAzure文心一言讯飞星火清华 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()
}
}

View File

@@ -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
View 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
View 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>

View File

@@ -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 URLhttps://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>

View File

@@ -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) {

View File

@@ -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")

View File

@@ -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)

View File

@@ -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++) {

View File

@@ -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++) {

View File

@@ -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";

View File

@@ -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
}
}