merge v4.0.8

This commit is contained in:
RockYang
2024-08-01 18:09:00 +08:00
76 changed files with 3392 additions and 1940 deletions

View File

@@ -6,4 +6,4 @@ VUE_APP_ADMIN_USER=admin
VUE_APP_ADMIN_PASS=admin123
VUE_APP_KEY_PREFIX=ChatPLUS_DEV_
VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.0.7
VUE_APP_VERSION=v4.0.8

View File

@@ -2,4 +2,4 @@ VUE_APP_API_HOST=
VUE_APP_WS_HOST=
VUE_APP_KEY_PREFIX=ChatPLUS_
VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.0.7
VUE_APP_VERSION=v4.0.8

View File

@@ -1,5 +1,5 @@
{
"name": "chatgpt-plus-web",
"name": "geekai-web",
"version": "0.1.0",
"private": true,
"scripts": {
@@ -20,8 +20,7 @@
"json-bigint": "^1.0.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.1",
"markdown-it-latex2img": "^0.0.6",
"markdown-it-mathjax": "^2.0.0",
"markdown-it-mathjax3": "^4.3.2",
"markmap-common": "^0.16.0",
"markmap-lib": "^0.16.1",
"markmap-toolbar": "^0.17.0",

View File

@@ -13,8 +13,9 @@
.item {
display flex
flex-flow row
border 1px solid #3c3c3c
border 1px solid rgb(80,80,80)
padding 10px
background rgba(60,60,60 0.5)
.image {
width 80px

View File

@@ -129,15 +129,44 @@ $borderColor = #4676d0;
--el-main-padding: 0;
margin: 0;
.chat-box {
.chat-container {
min-width: 0;
flex: 1;
background-color: var(--el-bg-color)
color var(--el-text-color-primary)
.chat-config {
height 30px
padding 10px 30px
display flex
justify-content center
justify-items center
border-bottom 1px solid #d9d9e3
.role-select-label {
color #ffffff
}
.el-select {
max-width 150px;
margin-right 10px;
}
.role-select {
max-width 130px;
}
.el-button {
.el-icon {
margin-right 5px;
}
}
}
#container {
overflow: hidden;
width: 100%;
position relative
::-webkit-scrollbar {
width: 0;
@@ -165,68 +194,97 @@ $borderColor = #4676d0;
}
.input-box {
background-color: #ffffff
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
padding 0 15px;
position absolute
bottom 0
width 100%
.tool-item {
margin-right 15px
border-radius: 6px;
color: #19c37d;
.input-box-inner {
display flex
justify-content center
justify-items center
padding 6px
cursor pointer
background #F2F2F2
background-color: #ffffff
justify-content: center;
align-items: center;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
padding 0 15px;
&:hover {
background #D5FAD3
}
.tool-item {
margin-right 15px
border-radius: 6px;
color: #19c37d;
display flex
justify-content center
justify-items center
padding 6px
cursor pointer
background #F2F2F2
.iconfont {
font-size: 24px;
}
}
&:hover {
background #D5FAD3
}
.input-container {
width 100%
margin: 0;
border: none;
padding: 10px 0;
display flex
justify-content center
position relative
.el-textarea {
.el-textarea__inner::-webkit-scrollbar {
width: 0;
height: 0;
.iconfont {
font-size: 24px;
}
}
.select-file {
position absolute;
right 48px;
top 20px;
}
.input-body {
width 100%
margin: 0;
border: none;
padding: 10px 0;
display flex
justify-content center
position relative
.send-btn {
position absolute;
right 12px;
top 20px;
.hide-div {
white-space: pre-wrap; /* */
visibility: hidden; /* div */
position: absolute; /* */
line-height: 24px
font-size 14px
word-wrap: break-word; /* */
overflow-wrap: break-word; /* */
}
.input-border {
display flex
width 100%
overflow hidden
border: 2px solid #21AA93
border-radius 10px
padding 10px
.prompt-input::-webkit-scrollbar {
width: 0;
height: 0;
}
.prompt-input {
width 100%
line-height: 24px
border none
font-size 14px
background none
resize: none
white-space: pre-wrap; /* */
word-wrap: break-word; /* */
overflow-wrap: break-word; /* */
}
.send-btn {
width 32px
margin-left 10px
.el-button {
padding 8px 5px;
border-radius 6px;
font-size 20px;
}
}
.el-button {
padding 8px 5px;
border-radius 6px;
font-size 20px;
}
}
}
}
}

View File

@@ -21,8 +21,8 @@
cursor pointer
.el-image {
width 40px
height 40px
width 48px
height 48px
}
}
@@ -86,7 +86,7 @@
.navigator {
display flex
flex-flow column
width 50px
width 60px
padding 10px 1px
border-right: 1px solid #3c3c3c
background-color: #1E1F22
@@ -97,12 +97,14 @@
li {
margin-bottom 15px
display flex
flex-flow column
a {
color #DADBDC
border-radius 10px
width 40px
height 40px
width 48px
height 48px
display flex
justify-content center
align-items center
@@ -125,9 +127,12 @@
.title {
font-size: 12px
padding-top: 5px
padding-top: 6px
color: #e5e7eb;
text-align: center;
white-space: nowrap; /* */
overflow: hidden; /* */
text-overflow: unset; /* 使 */
}
.active {

View File

@@ -50,6 +50,7 @@
display flex
cursor pointer
margin-bottom: 10px;
border 1px solid #383838
&:hover {
background-color #585858
@@ -61,60 +62,9 @@
margin-bottom 5px
}
.shape {
width 16px
height 16px
margin-bottom 5px
border 1px solid #C4C4C4
border-radius 3px
}
.shape.size9-16 {
width 9px
height 16px
}
.shape.size16-9 {
height 9px
width 16px
position relative
margin 4px 0 7px
}
.shape.size3-4 {
width 12px
height 16px
}
.shape.size4-3 {
height 12px
width 16px
position relative
margin 4px 0 4px
}
.shape.size2-3 {
width 11px
height 16px
}
.shape.size3-2 {
height 11px
width 16px
position relative
margin 4px 0 5px
}
.shape.size1-2 {
width 8px
height 16px
}
.shape.size2-1 {
height 8px
width 16px
position relative
margin 4px 0 8px
.text {
margin-left 5px
margin-top 2px
}
}
@@ -122,10 +72,7 @@
.grid-content.active {
color #47fff1
background-color #585858
.shape {
border 1px solid #47fff1
}
border 1px solid #47fff1
}
.model {

View File

@@ -92,9 +92,9 @@ const items = [
},
{
icon: 'role',
index: '/admin/role',
title: '角色管理',
icon: 'menu',
index: '/admin/app',
title: '应用管理',
},
{
icon: 'api-key',

View File

@@ -46,7 +46,7 @@ const routes = [
component: () => import('@/views/Member.vue'),
},
{
name: 'chat-role',
name: 'chat-app',
path: '/apps',
meta: {title: '应用中心'},
component: () => import('@/views/ChatApps.vue'),
@@ -139,10 +139,10 @@ const routes = [
component: () => import('@/views/admin/Users.vue'),
},
{
path: '/admin/role',
name: 'admin-role',
meta: {title: '角色管理'},
component: () => import('@/views/admin/Roles.vue'),
path: '/admin/app',
name: 'admin-app',
meta: {title: '应用管理'},
component: () => import('@/views/admin/Apps.vue'),
},
{
path: '/admin/apikey',

View File

@@ -65,9 +65,44 @@
</div>
</el-aside>
<el-main v-loading="loading" element-loading-background="rgba(122, 122, 122, 0.3)">
<div class="chat-box" :style="{height: mainWinHeight+'px'}">
<div class="chat-container">
<div class="chat-config">
<el-select v-model="roleId" filterable placeholder="角色" @change="_newChat"
class="role-select"
style="width:150px">
<el-option
v-for="item in roles"
:key="item.id"
:label="item.name"
:value="item.id"
>
<div class="role-option">
<el-image :src="item.icon"></el-image>
<span>{{ item.name }}</span>
</div>
</el-option>
</el-select>
<el-select v-model="modelID" filterable placeholder="模型" @change="_newChat"
:disabled="disableModel"
style="width:150px">
<el-option
v-for="item in models"
:key="item.id"
:label="item.name"
:value="item.id"
>
<span>{{ item.name }}</span>
<el-tag style="margin-left: 5px; position: relative; top:-2px" type="info" size="small">{{
item.power
}}算力
</el-tag>
</el-option>
</el-select>
</div>
<div>
<div id="container">
<div id="container" :style="{height: mainWinHeight+'px'}">
<div class="chat-box" id="chat-box" :style="{height: chatBoxHeight+'px'}">
<div v-if="showHello">
<welcome @send="autofillPrompt"/>
@@ -84,61 +119,8 @@
</div>
</div><!-- end chat box -->
<el-affix position="bottom" :offset="0">
<div class="input-box">
<span class="tool-item">
<el-popover
:width="300"
trigger="click"
placement="top-start"
>
<template #reference>
<div>
<el-tooltip effect="dark" content="模型选择">
<i class="iconfont icon-model"></i>
</el-tooltip>
</div>
</template>
<template #default>
<div class="chat-config">
<el-select v-model="roleId" filterable placeholder="角色" @change="_newChat"
class="role-select"
style="width:150px">
<el-option
v-for="item in roles"
:key="item.id"
:label="item.name"
:value="item.id"
>
<div class="role-option">
<el-image :src="item.icon"></el-image>
<span>{{ item.name }}</span>
</div>
</el-option>
</el-select>
<el-select v-model="modelID" filterable placeholder="模型" @change="_newChat"
:disabled="disableModel"
style="width:150px">
<el-option
v-for="item in models"
:key="item.id"
:label="item.name"
:value="item.id"
>
<span>{{ item.name }}</span>
<el-tag style="margin-left: 5px; position: relative; top:-2px" type="info" size="small">{{
item.power
}}算力
</el-tag>
</el-option>
</el-select>
</div>
</template>
</el-popover>
</span>
<div class="input-box">
<div class="input-box-inner">
<span class="tool-item" @click="ElMessage.info('暂时不支持语音输入')">
<el-tooltip class="box-item" effect="dark" content="语音输入">
<i class="iconfont icon-mic-bold"></i>
@@ -151,32 +133,34 @@
</el-tooltip>
</span>
<div class="input-container">
<el-input
ref="textInput"
v-model="prompt"
v-on:keydown="inputKeyDown"
autofocus
type="textarea"
:rows="2"
style="--el-input-focus-border-color:#21AA93;
border: 1px solid #21AA93;--el-input-border-color:#21AA93;
border-radius: 5px; --el-input-hover-border-color:#21AA93;"
placeholder="按 Enter 键发送消息,使用 Ctrl + Enter 换行"
/>
<span class="send-btn">
<el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
<el-icon>
<VideoPause/>
</el-icon>
</el-button>
<el-button @click="sendMessage" color="#19c37d" style="color:#ffffff" v-else>
<el-icon><Promotion/></el-icon>
</el-button>
</span>
<div class="input-body">
<div ref="textHeightRef" class="hide-div">{{prompt}}</div>
<div class="input-border">
<textarea
ref="inputRef"
class="prompt-input"
:rows="row"
v-model="prompt"
@keydown="onInput"
@input="onInput"
placeholder="按 Enter 键发送消息,使用 Ctrl + Enter 换行"
autofocus>
</textarea>
<span class="send-btn">
<el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
<el-icon>
<VideoPause/>
</el-icon>
</el-button>
<el-button @click="sendMessage" color="#19c37d" style="color:#ffffff" v-else>
<el-icon><Promotion/></el-icon>
</el-button>
</span>
</div>
</div>
</div><!-- end input box -->
</el-affix>
</div>
</div><!-- end container -->
</div><!-- end loading -->
</div>
@@ -237,11 +221,13 @@ const roleId = ref(0)
const newChatItem = ref(null);
const isLogin = ref(false)
const showHello = ref(true)
const textInput = ref(null)
const inputRef = ref(null)
const textHeightRef = ref(null)
const showNotice = ref(false)
const notice = ref("")
const noticeKey = ref("SYSTEM_NOTICE")
const store = useSharedStore();
const row = ref(1)
if (isMobile()) {
router.replace("/mobile/chat")
@@ -372,8 +358,8 @@ const getRoleById = function (rid) {
}
const resizeElement = function () {
chatBoxHeight.value = window.innerHeight - 50 - 82 - 38;
mainWinHeight.value = window.innerHeight - 50;
chatBoxHeight.value = window.innerHeight - 101 - 82 - 38;
mainWinHeight.value = window.innerHeight - 101;
leftBoxHeight.value = window.innerHeight - 90 - 45 - 82;
};
@@ -517,8 +503,7 @@ const removeChat = function (chat) {
}
const latexPlugin = require('markdown-it-latex2img')
const mathjaxPlugin = require('markdown-it-mathjax')
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
breaks: true,
html: true,
@@ -543,7 +528,6 @@ const md = require('markdown-it')({
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
}
});
md.use(latexPlugin)
md.use(mathjaxPlugin)
// 创建 socket 连接
@@ -705,8 +689,19 @@ const enableInput = () => {
showStopGenerate.value = false;
}
// 登录输入框输入事件处理
const inputKeyDown = function (e) {
const onInput = (e) => {
// 根据输入的内容自动计算输入框的行数
const lineHeight = parseFloat(window.getComputedStyle(inputRef.value).lineHeight)
textHeightRef.value.style.width = inputRef.value.clientWidth + 'px'; // 设定宽度和 textarea 相同
const lines = Math.floor(textHeightRef.value.clientHeight / lineHeight);
inputRef.value.scrollTo(0, inputRef.value.scrollHeight)
if (prompt.value.length < 10) {
row.value = 1
} else if (row.value <= 7) {
row.value = lines
}
// 输入回车自动提交
if (e.keyCode === 13) {
if (e.ctrlKey) { // Ctrl + Enter 换行
prompt.value += "\n";
@@ -720,7 +715,7 @@ const inputKeyDown = function (e) {
// 自动填充 prompt
const autofillPrompt = (text) => {
prompt.value = text
textInput.value.focus()
inputRef.value.focus()
// sendMessage()
}
// 发送消息
@@ -929,29 +924,4 @@ const insertURL = (url) => {
}
}
}
.chat-config {
display flex
flex-direction row
padding-top 10px;
.role-select-label {
color #ffffff
}
.el-select {
max-width 150px;
margin-right 10px;
}
.role-select {
max-width 130px;
}
.el-button {
.el-icon {
margin-right 5px;
}
}
}
</style>

View File

@@ -11,8 +11,8 @@
</div>
<div class="navbar">
<el-tooltip
v-if="!licenseConfig.de_copy"
class="box-item"
effect="light"
content="部署文档"
@@ -23,6 +23,7 @@
</el-tooltip>
<el-tooltip
v-if="!licenseConfig.de_copy"
class="box-item"
effect="light"
content="项目源码"
@@ -85,6 +86,7 @@
<el-image :src="item.icon" style="width: 30px;height: 30px"/>
</a>
</el-tooltip>
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
</li>
<el-popover
@@ -139,6 +141,7 @@ import {removeUserToken} from "@/store/session";
import LoginDialog from "@/components/LoginDialog.vue";
import {useSharedStore} from "@/store/sharedata";
import ConfigDialog from "@/components/ConfigDialog.vue";
import {showMessageError} from "@/utils/dialog";
const router = useRouter();
const logo = ref('/images/logo.png');
@@ -151,6 +154,7 @@ const loginUser = ref({})
const version = ref(process.env.VUE_APP_VERSION)
const routerViewKey = ref(0)
const showConfigDialog = ref(false)
const licenseConfig = ref({})
const store = useSharedStore();
const show = ref(false)
@@ -190,6 +194,12 @@ onMounted(() => {
ElMessage.error("获取系统菜单失败:" + e.message)
})
httpGet("/api/config/license").then(res => {
licenseConfig.value = res.data
}).catch(e => {
showMessageError("获取 License 配置:" + e.message)
})
init()
})

View File

@@ -1,6 +1,6 @@
<template>
<div class="index-page" :style="{height: winHeight+'px'}">
<div class="bg"></div>
<div :class="bgClass"></div>
<div class="menu-box">
<el-menu
mode="horizontal"
@@ -11,7 +11,8 @@
<div class="title">{{ title }}</div>
</div>
<div class="menu-item">
<a href="https://ai.r9it.com/docs/install/" target="_blank">
<span v-if="!licenseConfig.de_copy">
<a href="https://ai.r9it.com/docs/install/" target="_blank">
<el-button type="primary" round>
<i class="iconfont icon-book"></i>
<span>部署文档</span>
@@ -24,6 +25,7 @@
<span>项目源码</span>
</el-button>
</a>
</span>
<el-button @click="router.push('/login')" round>登录</el-button>
<el-button @click="router.push('/register')" round>注册</el-button>
</div>
@@ -52,7 +54,7 @@
<!-- <div id="animation-container"></div>-->
</div>
<div class="footer">
<div class="footer" v-if="!licenseConfig.de_copy">
<footer-bar />
</div>
</div>
@@ -77,73 +79,31 @@ if (isMobile()) {
const title = ref("Geek-AI 创作系统")
const logo = ref("/images/logo.png")
const slogan = ref("我辈之人,先干为敬,陪您先把 AI 用起来")
const licenseConfig = ref({})
// const size = Math.max(window.innerWidth * 0.5, window.innerHeight * 0.8)
const winHeight = window.innerHeight - 150
const bgClass = ref('fixed-bg')
onMounted(() => {
httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title
logo.value = res.data.logo
if (res.data.rand_bg) {
bgClass.value = "rand-bg"
}
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
httpGet("/api/config/license").then(res => {
licenseConfig.value = res.data
}).catch(e => {
ElMessage.error("获取 License 配置:" + e.message)
})
init()
})
const init = () => {
// // 创建场景
// // 创建场景
// const scene = new THREE.Scene();
//
// // 创建相机
// const camera = new THREE.PerspectiveCamera(30, 1, 0.1, 1000);
// camera.position.z = 3.88;
//
// // 创建渲染器
// const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
// renderer.setSize(size, size);
// renderer.setClearColor(0x000000, 0);
// const container = document.getElementById('animation-container');
// container.appendChild(renderer.domElement);
//
// // 加载地球纹理
// const loader = new THREE.TextureLoader();
// loader.load(
// '/images/land_ocean_ice_cloud_2048.jpg',
// function (texture) {
// // 创建地球球体
// const geometry = new THREE.SphereGeometry(1, 32, 32);
// const material = new THREE.MeshPhongMaterial({
// map: texture,
// bumpMap: texture, // 使用同一张纹理作为凹凸贴图
// bumpScale: 0.05, // 调整凹凸贴图的影响程度
// specularMap: texture, // 高光贴图
// specular: new THREE.Color('#01193B'), // 高光颜色
// });
// const earth = new THREE.Mesh(geometry, material);
// scene.add(earth);
//
// // 添加环境光和点光源
// const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
// scene.add(ambientLight);
// const pointLight = new THREE.PointLight(0xffffff, 0.8);
// pointLight.position.set(5, 5, 5);
// scene.add(pointLight);
//
// // 创建动画
// const animate = function () {
// requestAnimationFrame(animate);
//
// // 使地球自转和公转
// earth.rotation.y += 0.0006;
//
// renderer.render(scene, camera);
// };
//
// // 执行动画
// animate();
// }
// );
}
</script>
@@ -158,14 +118,25 @@ const init = () => {
align-items baseline
padding-top 150px
.bg {
.fixed-bg {
position absolute
top 0
left 0
width 100vw
height 100vh
background-image url("~@/assets/img/ai-bg.jpg")
//filter: blur(8px);
background-size: cover;
background-position: center;
}
.rand-bg {
position absolute
top 0
left 0
width 100vw
height 100vh
background-image url("https://api.dujin.org/bing/1920.php")
filter: blur(8px);
background-size: cover;
background-position: center;
}

View File

@@ -48,7 +48,7 @@
<reset-pass @hide="showResetPass = false" :show="showResetPass"/>
<footer class="footer">
<footer class="footer" v-if="!licenseConfig.de_copy">
<footer-bar/>
</footer>
</div>
@@ -74,6 +74,7 @@ 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 licenseConfig = ref({})
// 获取系统配置
httpGet("/api/config/get?key=system").then(res => {
@@ -83,6 +84,11 @@ httpGet("/api/config/get?key=system").then(res => {
showMessageError("获取系统配置失败:" + e.message)
})
httpGet("/api/config/license").then(res => {
licenseConfig.value = res.data
}).catch(e => {
showMessageError("获取 License 配置:" + e.message)
})
checkSession().then(() => {
if (isMobile()) {

View File

@@ -160,7 +160,7 @@
</el-result>
</div>
<footer class="footer">
<footer class="footer" v-if="!licenseConfig.de_copy">
<footer-bar/>
</footer>
</div>
@@ -199,6 +199,7 @@ const enableUser = ref(false)
const enableRegister = ref(false)
const activeName = ref("mobile")
const wxImg = ref("/images/wx.png")
const licenseConfig = ref({})
httpGet("/api/config/get?key=system").then(res => {
if (res.data) {
@@ -225,6 +226,12 @@ httpGet("/api/config/get?key=system").then(res => {
ElMessage.error("获取系统配置失败:" + e.message)
})
httpGet("/api/config/license").then(res => {
licenseConfig.value = res.data
}).catch(e => {
showMessageError("获取 License 配置:" + e.message)
})
// 注册操作
const submitRegister = () => {
if (data.value.username === '') {

View File

@@ -119,12 +119,12 @@ const mjModels = ref([
{name: "急速Turbo", value: "turbo"},
])
httpGet("/api/admin/config/get/draw").then(res => {
httpGet("/api/admin/config/get/app").then(res => {
sdConfigs.value = res.data.sd
mjPlusConfigs.value = res.data.mj_plus
mjProxyConfigs.value = res.data.mj_proxy
}).catch(e =>{
ElMessage.error("获取AI绘画配置失败:"+e.message)
ElMessage.error("获取配置失败:"+e.message)
})
const addConfig = (configs) => {

View File

@@ -2,6 +2,24 @@
<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"
:value="item.value"
/>
</el-select>
<el-button :icon="Search" @click="fetchData">搜索</el-button>
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
<a href="https://api.chat-plus.net" target="_blank" style="margin-left: 10px">
<el-button type="success" :icon="ShoppingCart" @click="add" plain>购买API-KEY</el-button>
@@ -87,7 +105,7 @@
<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="changePlatform">
<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
}}
@@ -99,7 +117,8 @@
</el-form-item>
<el-form-item label="API URL" prop="api_url">
<el-input v-model="item.api_url" autocomplete="off"
placeholder="如果你用了第三方的 API 中转这里填写中转地址"/>
placeholder="必须填土完整的 Chat API URLhttps://api.openai.com/v1/chat/completions"/>
<div class="info">如果你使用了第三方中转这里就填写中转地址</div>
</el-form-item>
<el-form-item label="代理地址:" prop="proxy_url">
@@ -126,11 +145,12 @@ import {onMounted, onUnmounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
import {DocumentCopy, Plus, ShoppingCart} from "@element-plus/icons-vue";
import {DocumentCopy, Plus, ShoppingCart, Search} from "@element-plus/icons-vue";
import ClipboardJS from "clipboard";
// 变量定义
const items = ref([])
const query = ref({type: '',platform:''})
const item = ref({})
const showDialog = ref(false)
const rules = reactive({
@@ -142,39 +162,7 @@ const rules = reactive({
const loading = ref(true)
const formRef = ref(null)
const title = ref("")
const platforms = ref([
{
name: "OpenAI/中转ChatGPT",
value: "OpenAI",
api_url: "https://api.chat-plus.net/v1/chat/completions",
img_url: "https://api.chat-plus.net/v1/images/generations"
},
{
name: "【讯飞】星火大模型",
value: "XunFei",
api_url: "wss://spark-api.xf-yun.com/{version}/chat"
},
{
name: "【清华智普】ChatGLM",
value: "ChatGLM",
api_url: "https://open.bigmodel.cn/api/paas/v3/model-api/{model}/sse-invoke"
},
{
name: "【百度】文心一言",
value: "Baidu",
api_url: "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}"
},
{
name: "【微软】Azure",
value: "Azure",
api_url: "https://chat-bot-api.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-05-15"
},
{
name: "【阿里】千义通问",
value: "QWen",
api_url: "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"
},
])
const platforms = ref([])
const types = ref([
{name: "聊天", value: "chat"},
{name: "绘画", value: "img"},
@@ -191,6 +179,14 @@ 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)
})
fetchData()
})
onUnmounted(() => {
@@ -198,19 +194,22 @@ onUnmounted(() => {
})
// 获取数据
httpGet('/api/admin/apikey/list').then((res) => {
if (res.data) {
// 初始化数据
const arr = res.data;
for (let i = 0; i < arr.length; i++) {
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
const fetchData = () => {
httpGet('/api/admin/apikey/list', query.value).then((res) => {
if (res.data) {
// 初始化数据
const arr = res.data;
for (let i = 0; i < arr.length; i++) {
arr[i].last_used_at = dateFormat(arr[i].last_used_at)
}
items.value = arr
}
items.value = arr
}
loading.value = false
}).catch(() => {
ElMessage.error("获取数据失败");
})
loading.value = false
}).catch(() => {
ElMessage.error("获取数据失败");
})
}
const add = function () {
showDialog.value = true
@@ -263,21 +262,24 @@ const set = (filed, row) => {
})
}
const changePlatform = () => {
let platform = null
const selectedPlatform = ref(null)
const changePlatform = (value) => {
console.log(value)
for (let v of platforms.value) {
if (v.value === item.value.platform) {
platform = v
break
if (v.value === value) {
selectedPlatform.value = v
item.value.api_url = v.chat_url
}
}
if (platform !== null) {
if (item.value.type === "img" && platform.img_url) {
item.value.api_url = platform.img_url
} else {
item.value.api_url = platform.api_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>
@@ -286,6 +288,10 @@ const changePlatform = () => {
.list {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
}
}
.copy-key {
@@ -306,7 +312,9 @@ const changePlatform = () => {
.el-form {
.el-form-item__content {
.info {
color #999999
}
.el-icon {
padding-left: 10px;
}

View File

@@ -9,25 +9,25 @@
<template #default="props">
<div>
<el-table :data="props.row.context" :border="childBorder">
<el-table-column label="对话角色" prop="role" width="120"/>
<el-table-column label="对话应用" prop="role" width="120"/>
<el-table-column label="对话内容" prop="content"/>
</el-table>
</div>
</template>
</el-table-column>
<el-table-column label="角色名称" prop="name">
<el-table-column label="应用名称" prop="name">
<template #default="scope">
<span class="sort" :data-id="scope.row.id">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="角色标识" prop="key"/>
<el-table-column label="应用标识" prop="key"/>
<el-table-column label="绑定模型" prop="model_name"/>
<el-table-column label="启用状态">
<template #default="scope">
<el-switch v-model="scope.row['enable']" @change="roleSet('enable',scope.row)"/>
</template>
</el-table-column>
<el-table-column label="角色图标" prop="icon">
<el-table-column label="应用图标" prop="icon">
<template #default="scope">
<el-image :src="scope.row.icon" style="width: 45px; height: 45px; border-radius: 50%"/>
</template>
@@ -36,7 +36,7 @@
<el-table-column label="操作" width="150" align="right">
<template #default="scope">
<el-button size="small" type="primary" @click="rowEdit(scope.$index, scope.row)">编辑</el-button>
<el-popconfirm title="确定要删除当前角色吗?" @confirm="removeRole(scope.row)" :width="200">
<el-popconfirm title="确定要删除当前应用吗?" @confirm="removeRole(scope.row)" :width="200">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
@@ -53,21 +53,21 @@
width="50%"
>
<el-form :model="role" label-width="120px" ref="formRef" label-position="left" :rules="rules">
<el-form-item label="角色名称:" prop="name">
<el-form-item label="应用名称:" prop="name">
<el-input
v-model="role.name"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="角色标志:" prop="key">
<el-form-item label="应用标志:" prop="key">
<el-input
v-model="role.key"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="角色图标:" prop="icon">
<el-form-item label="应用图标:" prop="icon">
<el-input v-model="role.icon">
<template #append>
<el-upload
@@ -107,7 +107,7 @@
<el-form-item label="上下文信息:" prop="context">
<template #default>
<el-table :data="role.context" :border="childBorder" size="small">
<el-table-column label="对话角色" width="120">
<el-table-column label="对话应用" width="120">
<template #default="scope">
<el-input
v-model="scope.row.role"
@@ -133,6 +133,8 @@
<template #default="scope">
<div class="context-msg-content">
<el-input
type="textarea"
:rows="2"
v-model="scope.row.content"
autocomplete="off"
/>
@@ -181,8 +183,8 @@ const loading = ref(true)
const rules = reactive({
name: [{required: true, message: '请输入用户名', trigger: 'blur',}],
key: [{required: true, message: '请输入角色标识', trigger: 'blur',}],
icon: [{required: true, message: '请输入角色图标', trigger: 'blur',}],
key: [{required: true, message: '请输入应用标识', trigger: 'blur',}],
icon: [{required: true, message: '请输入应用图标', trigger: 'blur',}],
sort: [
{required: true, message: '请输入排序数字', trigger: 'blur'},
{type: 'number', message: '请输入有效数字'},
@@ -204,13 +206,13 @@ onMounted(() => {
})
const fetchData = () => {
//
//
httpGet('/api/admin/role/list').then((res) => {
tableData.value = res.data
sortedTableData.value = copyObj(tableData.value)
loading.value = false
}).catch(() => {
ElMessage.error("获取聊天角色失败");
ElMessage.error("获取聊天应用失败");
})
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
@@ -250,14 +252,14 @@ const roleSet = (filed, row) => {
//
const curIndex = ref(0)
const rowEdit = function (index, row) {
optTitle.value = "修改角色"
optTitle.value = "修改应用"
curIndex.value = index
role.value = copyObj(row)
showDialog.value = true
}
const addRole = function () {
optTitle.value = "添加新角色"
optTitle.value = "添加新应用"
role.value = {context: []}
showDialog.value = true
}

View File

@@ -163,11 +163,8 @@
:model="item.model"
:content="item.content"/>
<chat-reply v-else-if="item.type==='reply'"
:icon="item.icon"
:org-content="item.orgContent"
:created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']"
:content="item.content"/>
:read-only="true"
:data="item"/>
</div>
</div><!-- end chat box -->
</el-dialog>
@@ -291,8 +288,7 @@ const removeMessage = function (row) {
})
}
const latexPlugin = require('markdown-it-latex2img')
const mathjaxPlugin = require('markdown-it-mathjax')
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
breaks: true,
html: true,
@@ -312,7 +308,6 @@ const md = require('markdown-it')({
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`
}
});
md.use(latexPlugin)
md.use(mathjaxPlugin)
const showContentDialog = ref(false)

View File

@@ -2,6 +2,16 @@
<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-button :icon="Search" @click="fetchData">搜索</el-button>
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
</div>
@@ -198,12 +208,13 @@ import {onMounted, onUnmounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
import {DocumentCopy, InfoFilled, Plus} from "@element-plus/icons-vue";
import {DocumentCopy, InfoFilled, Plus,Search} from "@element-plus/icons-vue";
import {Sortable} from "sortablejs";
import ClipboardJS from "clipboard";
// 变量定义
const items = ref([])
const query = ref({platform:''})
const item = ref({})
const showDialog = ref(false)
const title = ref("")
@@ -214,15 +225,7 @@ const rules = reactive({
})
const loading = ref(true)
const formRef = ref(null)
const platforms = ref([
{name: "OpenAIChatGPT", value: "OpenAI"},
{name: "讯飞星火大模型", value: "XunFei"},
{name: "清华智普ChatGLM", value: "ChatGLM"},
{name: "百度文心一言", value: "Baidu"},
{name: "微软Azure", value: "Azure"},
{name: "阿里通义千问", value: "QWen"},
])
const platforms = ref([])
// 获取 API KEY
const apiKeys = ref([])
@@ -234,7 +237,7 @@ httpGet('/api/admin/apikey/list?status=true&type=chat').then(res => {
// 获取数据
const fetchData = () => {
httpGet('/api/admin/model/list').then((res) => {
httpGet('/api/admin/model/list', query.value).then((res) => {
if (res.data) {
// 初始化数据
const arr = res.data;
@@ -287,6 +290,12 @@ 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(() => {
@@ -352,6 +361,10 @@ const remove = function (row) {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
}
}
.cell {

View File

@@ -44,6 +44,20 @@
</el-tooltip>
</el-form-item>
<el-form-item label="随机背景">
<el-switch v-model="system['rand_bg']"/>
<el-tooltip
effect="dark"
content="打开之后前端首页将使用随机壁纸作为背景图"
raw-content
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</el-form-item>
<el-form-item label="注册方式" prop="register_ways">
<el-checkbox-group v-model="system['register_ways']">
<el-checkbox value="mobile">手机注册</el-checkbox>
@@ -298,16 +312,22 @@
<el-descriptions
v-if="license.is_active"
class="margin-top"
title="授权信息"
:column="3"
title="授权信息"
:column="1"
border
>
<el-descriptions-item :span="3" :width="150">
<el-descriptions-item>
<template #label>
<div class="cell-item">License Key</div>
</template>
{{ license.key }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">机器码</div>
</template>
{{ license.machine_id }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">到期时间</div>
@@ -318,13 +338,15 @@
<template #label>
<div class="cell-item">用户人数</div>
</template>
{{ license.user_num }}
{{ license.configs?.user_num }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">机器码</div>
<div class="cell-item">去版权</div>
</template>
{{ license.machine_id }}
<el-icon class="selected" v-if="license.configs?.de_copy"><Select /></el-icon>
<el-icon class="closed" v-else><CloseBold /></el-icon>
<span class="text">去版权之后前端页面将不会显示版权信息和源码地址</span>
</el-descriptions-item>
</el-descriptions>
@@ -348,7 +370,7 @@ import {onMounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import Compressor from "compressorjs";
import {ElMessage} from "element-plus";
import {InfoFilled, UploadFilled} from "@element-plus/icons-vue";
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";
@@ -536,6 +558,23 @@ const onUploadImg = (files, callback) => {
.el-descriptions {
margin-bottom 20px
.el-icon {
font-size 18px
}
.selected {
color #0bc15f
}
.closed {
color #da0d54
}
.text {
margin-left 10px
font-size 12px
color #999999
position: relative;
top -5px
}
}
.el-alert {

View File

@@ -354,6 +354,8 @@ const doResetPass = () => {
.pagination {
padding 20px 0
display flex
justify-content right
width 100%
}
.el-select {

View File

@@ -62,6 +62,7 @@ const model = ref('')
const finished = ref(false)
const error = ref(false)
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
breaks: true,
html: true,
@@ -86,6 +87,7 @@ const md = require('markdown-it')({
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
}
});
md.use(mathjaxPlugin)
const onLoad = () => {
httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
@@ -127,7 +129,7 @@ const onLoad = () => {
};
</script>
<style lang="stylus">
<style scoped lang="stylus">
.chat-export-mobile {
height 100vh
@@ -144,7 +146,7 @@ const onLoad = () => {
.message-list-box {
background #F5F5F5;
padding-bottom: 10px
padding-bottom: 50px
.van-cell {
background none

View File

@@ -225,18 +225,6 @@ const newChat = (item) => {
}
showPicker.value = false
const options = item.selectedOptions
// setChatConfig({
// role: {
// id: options[0].value,
// name: options[0].text,
// icon: options[0].icon,
// helloMsg: options[0].helloMsg
// },
// model: options[1].value,
// modelValue: getModelValue(options[1].value),
// title: '新建会话',
// chatId: 0
// })
router.push({
name: "mobile-chat-session",
params: {role_id: options[0].value, model_id: options[1].value, title: '新建会话', chat_id: 0}
@@ -244,25 +232,6 @@ const newChat = (item) => {
}
const changeChat = (chat) => {
// let role = {}
// for (let i = 0; i < roles.value.length; i++) {
// if (roles.value[i].value === chat.role_id) {
// role = roles.value[i]
// break
// }
// }
// setChatConfig({
// role: {
// id: chat.role_id,
// name: role.text,
// icon: role.icon
// },
// model: chat.model_id,
// modelValue: getModelValue(chat.model_id),
// title: chat.title,
// chatId: chat.chat_id,
// helloMsg: chat.hello_msg,
// })
router.push(`/mobile/chat/session?chat_id=${chat.chat_id}`)
}

View File

@@ -182,7 +182,7 @@ httpGet('/api/model/list').then(res => {
models.value[i].mValue = models.value[i].value
models.value[i].value = models.value[i].id
}
modelValue.value = getModelValue(modelId.value)
modelValue.value = getModelName(modelId.value)
// 加载角色列表
httpGet(`/api/role/list`).then((res) => {
roles.value = res.data;
@@ -236,6 +236,7 @@ const newChat = (item) => {
const options = item.selectedOptions
roleId.value = options[0].value
modelId.value = options[1].value
modelValue.value = getModelName(modelId.value)
chatId.value = ""
chatData.value = []
role.value = getRoleById(roleId.value)
@@ -248,8 +249,7 @@ const loading = ref(false)
const finished = ref(false)
const error = ref(false)
const latexPlugin = require('markdown-it-latex2img')
const mathjaxPlugin = require('markdown-it-mathjax')
const mathjaxPlugin = require('markdown-it-mathjax3')
const md = require('markdown-it')({
breaks: true,
html: true,
@@ -274,7 +274,6 @@ const md = require('markdown-it')({
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
}
});
md.use(latexPlugin)
md.use(mathjaxPlugin)
@@ -537,7 +536,7 @@ const shareChat = (option) => {
showToast({message: "当前会话已经导出,请通过浏览器或者微信的自带分享功能分享给好友", duration: 5000})
router.push({
path: "/mobile/chat/export",
query: {title: title, chat_id: chatId, role: role.value.name, model: modelValue}
query: {title: title.value, chat_id: chatId.value, role: role.value.name, model: modelValue.value}
})
} else if (option.icon === "link") {
document.getElementById('copy-link-btn').click();
@@ -553,10 +552,10 @@ const getRoleById = function (rid) {
return null;
}
const getModelValue = (model_id) => {
const getModelName = (model_id) => {
for (let i = 0; i < models.value.length; i++) {
if (models.value[i].id === model_id) {
return models.value[i].mValue
return models.value[i].text
}
}
return ""

View File

@@ -8,7 +8,7 @@
<image-sd/>
</van-tab>
<van-tab title="DALL" name="dall">
<van-empty description="功能正在开发中"/>
<image-dall />
</van-tab>
</van-tabs>
</div>
@@ -18,6 +18,7 @@
import {ref} from "vue";
import ImageMj from "@/views/mobile/pages/ImageMj.vue";
import ImageSd from "@/views/mobile/pages/ImageSd.vue";
import ImageDall from "@/views/mobile/pages/ImageDall.vue";
const activeName = ref("mj")
</script>
@@ -30,4 +31,4 @@ const activeName = ref("mj")
}
}
}
</style>
</style>

View File

@@ -0,0 +1,473 @@
<template>
<div class="mobile-sd">
<van-form @submit="generate">
<van-cell-group inset>
<div>
<van-field
v-model="quality"
is-link
label="图片质量"
placeholder="选择图片质量"
@click="showQualityPicker = true"
/>
<van-popup v-model:show="showQualityPicker" position="bottom" teleport="#app">
<van-picker
:columns="qualities"
@cancel="showQualityPicker = false"
@confirm="qualityConfirm"
/>
</van-popup>
</div>
<div>
<van-field
v-model="size"
is-link
label="图片尺寸"
placeholder="选择图片尺寸"
@click="showSizePicker = true"
/>
<van-popup v-model:show="showSizePicker" position="bottom" teleport="#app">
<van-picker
:columns="sizes"
@cancel="showSizePicker = false"
@confirm="sizeConfirm"
/>
</van-popup>
</div>
<div>
<van-field
v-model="style"
is-link
label="图片样式"
placeholder="选择图片样式"
@click="showStylePicker = true"
/>
<van-popup v-model:show="showStylePicker" position="bottom" teleport="#app">
<van-picker
:columns="styles"
@cancel="showStylePicker = false"
@confirm="styleConfirm"
/>
</van-popup>
</div>
<van-field
v-model="params.prompt"
rows="3"
autosize
type="textarea"
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
/>
<div class="text-line pt-6">
<el-tag>绘图消耗{{ dallPower }}算力当前算力{{ power }}</el-tag>
</div>
<div class="text-line">
<van-button round block type="primary" native-type="submit">
立即生成
</van-button>
</div>
</van-cell-group>
</van-form>
<h3>任务列表</h3>
<div class="running-job-list">
<van-empty v-if="runningJobs.length ===0"
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
image-size="80"
description="暂无记录"
/>
<van-grid :gutter="10" :column-num="3" v-else>
<van-grid-item v-for="item in runningJobs">
<div v-if="item.progress > 0">
<van-image :src="item['img_url']">
<template v-slot:error>加载失败</template>
</van-image>
<div class="progress">
<van-circle
v-model:current-rate="item.progress"
:rate="item.progress"
:speed="100"
:text="item.progress+'%'"
:stroke-width="60"
size="90px"
/>
</div>
</div>
<div v-else class="task-in-queue">
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
<span class="text">排队中</span>
</div>
</van-grid-item>
</van-grid>
</div>
<h3>创作记录</h3>
<div class="finish-job-list">
<van-empty v-if="finishedJobs.length ===0"
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
image-size="80"
description="暂无记录"
/>
<van-list v-else
v-model:error="error"
v-model:loading="loading"
:finished="finished"
error-text="请求失败点击重新加载"
finished-text="没有更多了"
@load="onLoad"
>
<van-grid :gutter="10" :column-num="2">
<van-grid-item v-for="item in finishedJobs">
<div class="job-item">
<van-image
:src="item['img_url']"
:class="item['can_opt'] ? '' : 'upscale'"
lazy-load
@click="imageView(item)"
fit="cover">
<template v-slot:loading>
<van-loading type="spinner" size="20"/>
</template>
</van-image>
<div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle/>
<el-button type="warning" v-if="item.publish" @click="publishImage($event,item, false)"
circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
<el-button type="primary" @click="showPrompt(item)" circle>
<i class="iconfont icon-prompt"></i>
</el-button>
</div>
</div>
</van-grid-item>
</van-grid>
</van-list>
</div>
<button style="display: none" class="copy-prompt-dall" :data-clipboard-text="prompt" id="copy-btn-dall">复制</button>
</div>
</template>
<script setup>
import {onMounted, onUnmounted, ref} from "vue"
import {Delete} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http";
import Clipboard from "clipboard";
import {checkSession} from "@/action/session";
import {useRouter} from "vue-router";
import {getSessionId} from "@/store/session";
import {
showConfirmDialog,
showDialog,
showFailToast,
showImagePreview,
showNotify,
showSuccessToast,
showToast
} from "vant";
import {showLoginDialog} from "@/utils/libs";
const listBoxHeight = ref(window.innerHeight - 40)
const mjBoxHeight = ref(window.innerHeight - 150)
const item = ref({})
const isLogin = ref(false)
const activeColspan = ref([""])
window.onresize = () => {
listBoxHeight.value = window.innerHeight - 40
mjBoxHeight.value = window.innerHeight - 150
}
const qualities = [
{text: "标准", value: "standard"},
{text: "高清", value: "hd"},
]
const sizes = [
{text:"1024x1024",value:"1024x1024"},
{text:"1792x1024",value:"1792x1024"},
{text: "1024x1792",value:"1024x1792"},
]
const styles = [
{text: "生动", value: "vivid"},
{text: "自然", value: "natural"}
]
const params = ref({
quality: qualities[0].value,
size: sizes[0].value,
style: styles[0].value,
prompt: ""
})
const quality = ref(qualities[0].text)
const size = ref(sizes[0].text)
const style = ref(styles[0].text)
const showQualityPicker = ref(false)
const showStylePicker = ref(false)
const showSizePicker = ref(false)
const runningJobs = ref([])
const finishedJobs = ref([])
const router = useRouter()
const power = ref(0)
const dallPower = ref(0) // 画一张 DALL 图片消耗算力
const socket = ref(null)
const userId = ref(0)
const heartbeatHandle = ref(null)
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 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/dall/client?user_id=${userId.value}`);
_socket.addEventListener('open', () => {
socket.value = _socket;
// 发送心跳消息
sendHeartbeat()
});
_socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
fetchRunningJobs()
finished.value = false
page.value = 1
fetchFinishJobs(page.value)
}
});
_socket.addEventListener('close', () => {
if (socket.value !== null) {
connect()
}
});
}
const clipboard = ref(null)
const prompt = ref('')
onMounted(() => {
initData()
clipboard.value = new Clipboard(".copy-prompt-dall");
clipboard.value.on('success', () => {
showNotify({type: 'success', message: '复制成功', duration: 1000})
})
clipboard.value.on('error', () => {
showNotify({type: 'danger', message: '复制失败', duration: 2000})
})
httpGet("/api/config/get?key=system").then(res => {
dallPower.value = res.data.dall_power
}).catch(e => {
showNotify({type: "danger", message: "获取系统配置失败:" + e.message})
})
})
onUnmounted(() => {
clipboard.value.destroy()
if (socket.value !== null) {
socket.value.close()
socket.value = null
}
})
const initData = () => {
checkSession().then(user => {
power.value = user['power']
userId.value = user.id
isLogin.value = true
fetchRunningJobs()
fetchFinishJobs(1)
connect()
}).catch(() => {
loading.value = false
});
}
const fetchRunningJobs = () => {
// 获取运行中的任务
httpGet(`/api/dall/jobs?status=0`).then(res => {
const jobs = res.data
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
showNotify({
message: `任务ID${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
type: 'danger',
})
power.value += dallPower.value
continue
}
_jobs.push(jobs[i])
}
runningJobs.value = _jobs
}).catch(e => {
showNotify({type: "danger", message: "获取任务失败:" + e.message})
})
}
const loading = ref(false)
const finished = ref(false)
const error = ref(false)
const page = ref(0)
const pageSize = ref(10)
// 获取已完成的任务
const fetchFinishJobs = (page) => {
loading.value = true
httpGet(`/api/dall/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
if (res.data.length < pageSize.value) {
finished.value = true
}
if (page === 1) {
finishedJobs.value = res.data
} else {
finishedJobs.value = finishedJobs.value.concat(res.data)
}
loading.value = false
}).catch(e => {
loading.value = false
showNotify({type: "danger", message: "获取任务失败:" + e.message})
})
}
const onLoad = () => {
page.value += 1
fetchFinishJobs(page.value)
}
// 创建绘图任务
const promptRef = ref(null)
const generate = () => {
if (!isLogin.value) {
return showLoginDialog(router)
}
if (params.value.prompt === '') {
promptRef.value.focus()
return showToast("请输入绘画提示词!")
}
if (!params.value.seed) {
params.value.seed = -1
}
params.value.session_id = getSessionId()
httpPost("/api/dall/image", params.value).then(() => {
showSuccessToast("绘画任务推送成功,请耐心等待任务执行...")
power.value -= dallPower.value
}).catch(e => {
showFailToast("任务推送失败:" + e.message)
})
}
const showPrompt = (item) => {
prompt.value = item.prompt
showConfirmDialog({
title: "绘画提示词",
message: item.prompt,
confirmButtonText: "复制",
cancelButtonText: "关闭",
}).then(() => {
document.querySelector('#copy-btn-dall').click()
}).catch(() => {
});
}
const removeImage = (event, item) => {
event.stopPropagation()
showConfirmDialog({
title: '标题',
message:
'此操作将会删除任务和图片,继续操作码?',
}).then(() => {
httpPost("/api/dall/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
showSuccessToast("任务删除成功")
}).catch(e => {
showFailToast("任务删除失败:" + e.message)
})
}).catch(() => {
showToast("您取消了操作")
});
}
// 发布图片到作品墙
const publishImage = (event, item, action) => {
event.stopPropagation()
let text = "图片发布"
if (action === false) {
text = "取消发布"
}
httpPost("/api/dall/publish", {id: item.id, action: action}).then(() => {
showSuccessToast(text + "成功")
item.publish = action
}).catch(e => {
showFailToast(text + "失败:" + e.message)
})
}
const imageView = (item) => {
showImagePreview([item['img_url']]);
}
const qualityConfirm = (item) => {
params.value.quality = item.selectedOptions[0].value;
quality.value = item.selectedOptions[0].text
showQualityPicker.value = false
}
const styleConfirm = (item) => {
params.value.style = item.selectedOptions[0].value;
style.value = item.selectedOptions[0].text
showStylePicker.value = false
}
const sizeConfirm =(item) => {
params.value.size = item.selectedOptions[0].value
size.value=item.selectedOptions[0].text
showSizePicker.value =false
}
const showInfo = (message) => {
showDialog({
title: "参数说明",
message: message,
}).then(() => {
// on close
});
}
</script>
<style lang="stylus">
@import "@/assets/css/mobile/image-sd.styl"
</style>

View File

@@ -43,8 +43,25 @@
</van-cell>
</van-list>
</van-tab>
<van-tab title="DALL" name="dalle3">
<van-empty description="功能正在开发中"/>
<van-tab title="DALL" name="dall">
<van-list
v-model:error="data['dall'].error"
v-model:loading="data['dall'].loading"
:finished="data['dall'].finished"
error-text="请求失败点击重新加载"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell v-for="item in data['dall'].data" :key="item.id">
<van-image :src="item['img_thumb']" @click="imageView(item)" fit="cover"/>
<div class="opt-box">
<el-button type="primary" @click="showPrompt(item)" circle>
<i class="iconfont icon-prompt"></i>
</el-button>
</div>
</van-cell>
</van-list>
</van-tab>
</van-tabs>
</div>
@@ -81,13 +98,13 @@ const data = ref({
url: "/api/sd/imgWall",
data: []
},
"dalle3": {
"dall": {
loading: false,
finished: false,
error: false,
page: 1,
pageSize: 12,
url: "/api/dalle3/imgWall",
url: "/api/dall/imgWall",
data: []
}
})