upgrade to v4.0.4
@@ -22,11 +22,15 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-latex2img": "^0.0.6",
|
||||
"markdown-it-mathjax": "^2.0.0",
|
||||
"markmap-common": "^0.16.0",
|
||||
"markmap-lib": "^0.16.1",
|
||||
"markmap-view": "^0.16.0",
|
||||
"md-editor-v3": "^2.2.1",
|
||||
"pinia": "^2.1.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"qs": "^6.11.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"three": "^0.128.0",
|
||||
"v3-waterfall": "^1.2.1",
|
||||
"vant": "^4.5.0",
|
||||
"vue": "^3.2.13",
|
||||
|
||||
BIN
web/public/images/avatar/seller.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
web/public/images/land_ocean_ice_cloud_2048.jpg
Normal file
|
After Width: | Height: | Size: 580 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 250 KiB |
BIN
web/public/images/menu/dalle.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
web/public/images/menu/more.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
web/public/images/menu/xmind.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
@@ -47,6 +47,7 @@
|
||||
|
||||
.opt {
|
||||
position: relative;
|
||||
width 100%
|
||||
top -5px
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ $borderColor = #4676d0;
|
||||
|
||||
.el-aside {
|
||||
background-color: $sideBgColor;
|
||||
height 100vh
|
||||
|
||||
.title-box {
|
||||
padding: 6px 10px;
|
||||
|
||||
88
web/src/assets/css/image-dall.styl
Normal file
@@ -0,0 +1,88 @@
|
||||
.page-dall {
|
||||
background-color: #282c34;
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
|
||||
.sd-box {
|
||||
margin 10px
|
||||
background-color #262626
|
||||
border 1px solid #454545
|
||||
min-width 300px
|
||||
max-width 300px
|
||||
padding 10px
|
||||
border-radius 10px
|
||||
color #ffffff;
|
||||
font-size 14px
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
font-size 20px
|
||||
text-align center
|
||||
color #47fff1
|
||||
}
|
||||
|
||||
// 隐藏滚动条
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.sd-params {
|
||||
margin-top 10px
|
||||
overflow auto
|
||||
|
||||
|
||||
.param-line {
|
||||
padding 0 10px
|
||||
|
||||
.grid-content
|
||||
.form-item-inner {
|
||||
display flex
|
||||
|
||||
.info-icon {
|
||||
margin-left 10px
|
||||
position relative
|
||||
top 8px
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.param-line.pt {
|
||||
padding-top 5px
|
||||
padding-bottom 5px
|
||||
}
|
||||
|
||||
.text-info {
|
||||
padding 10px
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding 10px 15px 0 15px
|
||||
text-align center
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
|
||||
span {
|
||||
color #2D3A4B
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
@import "task-list.styl"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,24 +38,14 @@
|
||||
.param-line {
|
||||
padding 0 10px
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 3px
|
||||
}
|
||||
|
||||
.el-input__suffix-inner {
|
||||
.el-icon {
|
||||
top 0
|
||||
}
|
||||
}
|
||||
|
||||
.grid-content
|
||||
.form-item-inner {
|
||||
display flex
|
||||
|
||||
.el-icon {
|
||||
.info-icon {
|
||||
margin-left 10px
|
||||
margin-top 2px
|
||||
position relative
|
||||
top 8px
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +58,6 @@
|
||||
|
||||
.text-info {
|
||||
padding 10px
|
||||
|
||||
.el-tag {
|
||||
margin-right 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
134
web/src/assets/css/mark-map.styl
Normal file
@@ -0,0 +1,134 @@
|
||||
.page-mark-map {
|
||||
background-color: #282c34;
|
||||
height 100vh
|
||||
|
||||
.inner {
|
||||
display: flex;
|
||||
|
||||
.mark-map-box {
|
||||
margin 10px
|
||||
background-color #262626
|
||||
border 1px solid #454545
|
||||
min-width 300px
|
||||
max-width 300px
|
||||
padding 10px
|
||||
border-radius 10px
|
||||
color #ffffff;
|
||||
font-size 14px
|
||||
|
||||
h2 {
|
||||
font-weight: bold;
|
||||
font-size 20px
|
||||
text-align center
|
||||
color #47fff1
|
||||
}
|
||||
|
||||
// 隐藏滚动条
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.mark-map-params {
|
||||
margin-top 10px
|
||||
overflow auto
|
||||
|
||||
|
||||
.param-line {
|
||||
padding 10px
|
||||
|
||||
.el-button {
|
||||
width 100%
|
||||
|
||||
span {
|
||||
color #2D3A4B
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.text-info {
|
||||
padding 10px
|
||||
|
||||
.el-tag {
|
||||
margin-right 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color #ffffff
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
width 100%
|
||||
|
||||
.top-bar {
|
||||
display flex
|
||||
justify-content space-between
|
||||
align-items center
|
||||
|
||||
h2 {
|
||||
color #ffffff
|
||||
}
|
||||
|
||||
.el-button {
|
||||
margin-right 20px
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
color #ffffff
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
h1 {
|
||||
color: #47fff1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 20px;
|
||||
|
||||
li {
|
||||
line-height 1.5
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
|
||||
.markmap {
|
||||
width 100%
|
||||
color #ffffff
|
||||
font-size 12px
|
||||
|
||||
.markmap-foreign {
|
||||
//height 30px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4125778 */
|
||||
src: url('iconfont.woff2?t=1708054962140') format('woff2'),
|
||||
url('iconfont.woff?t=1708054962140') format('woff'),
|
||||
url('iconfont.ttf?t=1708054962140') format('truetype');
|
||||
src: url('iconfont.woff2?t=1713766977199') format('woff2'),
|
||||
url('iconfont.woff?t=1713766977199') format('woff'),
|
||||
url('iconfont.ttf?t=1713766977199') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,38 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-more:before {
|
||||
content: "\e63c";
|
||||
}
|
||||
|
||||
.icon-mj:before {
|
||||
content: "\e643";
|
||||
}
|
||||
|
||||
.icon-dalle:before {
|
||||
content: "\e646";
|
||||
}
|
||||
|
||||
.icon-xmind:before {
|
||||
content: "\e610";
|
||||
}
|
||||
|
||||
.icon-version:before {
|
||||
content: "\e68d";
|
||||
}
|
||||
|
||||
.icon-sd:before {
|
||||
content: "\e62b";
|
||||
}
|
||||
|
||||
.icon-huihua1:before {
|
||||
content: "\e606";
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: "\e68a";
|
||||
}
|
||||
|
||||
.icon-prompt:before {
|
||||
content: "\e6ce";
|
||||
}
|
||||
|
||||
@@ -5,6 +5,62 @@
|
||||
"css_prefix_text": "icon-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "1421807",
|
||||
"name": "更多",
|
||||
"font_class": "more",
|
||||
"unicode": "e63c",
|
||||
"unicode_decimal": 58940
|
||||
},
|
||||
{
|
||||
"icon_id": "36264781",
|
||||
"name": "MidJourney",
|
||||
"font_class": "mj",
|
||||
"unicode": "e643",
|
||||
"unicode_decimal": 58947
|
||||
},
|
||||
{
|
||||
"icon_id": "37677137",
|
||||
"name": "DALL·E 3",
|
||||
"font_class": "dalle",
|
||||
"unicode": "e646",
|
||||
"unicode_decimal": 58950
|
||||
},
|
||||
{
|
||||
"icon_id": "2629858",
|
||||
"name": "逻辑图",
|
||||
"font_class": "xmind",
|
||||
"unicode": "e610",
|
||||
"unicode_decimal": 58896
|
||||
},
|
||||
{
|
||||
"icon_id": "1061336",
|
||||
"name": "version",
|
||||
"font_class": "version",
|
||||
"unicode": "e68d",
|
||||
"unicode_decimal": 59021
|
||||
},
|
||||
{
|
||||
"icon_id": "3901033",
|
||||
"name": "绘画",
|
||||
"font_class": "sd",
|
||||
"unicode": "e62b",
|
||||
"unicode_decimal": 58923
|
||||
},
|
||||
{
|
||||
"icon_id": "39185683",
|
||||
"name": "绘画",
|
||||
"font_class": "huihua1",
|
||||
"unicode": "e606",
|
||||
"unicode_decimal": 58886
|
||||
},
|
||||
{
|
||||
"icon_id": "2341972",
|
||||
"name": "chat",
|
||||
"font_class": "chat",
|
||||
"unicode": "e68a",
|
||||
"unicode_decimal": 59018
|
||||
},
|
||||
{
|
||||
"icon_id": "8017627",
|
||||
"name": "prompt",
|
||||
|
||||
@@ -90,6 +90,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
width 100%
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -93,6 +93,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
width 100%
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="foot-container">
|
||||
<div class="footer">
|
||||
Powered by {{ author }} @
|
||||
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
|
||||
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank" style="--el-link-text-color:#ffffff">
|
||||
{{ title }} -
|
||||
{{ version }}
|
||||
</el-link>
|
||||
@@ -19,7 +19,7 @@ const author = ref('极客学长')
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.container {
|
||||
.foot-container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
|
||||
@@ -226,10 +226,9 @@ import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {setUserToken} from "@/store/session";
|
||||
import {validateEmail, validateMobile} from "@/utils/validate";
|
||||
import {Checked, Close, Iphone, Lock, Message, Position, User} from "@element-plus/icons-vue";
|
||||
import {Checked, Close, Iphone, Lock, Message} from "@element-plus/icons-vue";
|
||||
import SendMsg from "@/components/SendMsg.vue";
|
||||
import {arrayContains} from "@/utils/libs";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
@@ -359,7 +358,7 @@ const close = function () {
|
||||
.close-icon {
|
||||
cursor pointer
|
||||
position absolute
|
||||
right -10px
|
||||
right 0
|
||||
top 0
|
||||
font-weight normal
|
||||
font-size 20px
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
<span>{{ sysTitle }}</span>
|
||||
</el-dropdown-item>
|
||||
</a>
|
||||
<el-dropdown-item>
|
||||
<i class="iconfont icon-version"></i> 当前版本:{{ version }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="showDialog = true">
|
||||
<i class="iconfont icon-reward"></i>
|
||||
<span>打赏作者</span>
|
||||
@@ -86,6 +89,7 @@ import {removeAdminToken} from "@/store/session";
|
||||
|
||||
const message = ref(5);
|
||||
const sysTitle = ref(process.env.VUE_APP_TITLE)
|
||||
const version = ref(process.env.VUE_APP_VERSION)
|
||||
const avatar = ref('/images/user-info.jpg')
|
||||
const donateImg = ref('/images/wechat-pay.png')
|
||||
const showDialog = ref(false)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="sidebar">
|
||||
<div class="logo">
|
||||
<el-image :src="logo"/>
|
||||
<span class="text" v-show="!sidebar.collapse">{{ title }} - {{ version }}</span>
|
||||
<span class="text" v-show="!sidebar.collapse">{{ title }}</span>
|
||||
</div>
|
||||
|
||||
<el-menu
|
||||
@@ -60,11 +60,11 @@ import {ElMessage} from "element-plus";
|
||||
|
||||
const title = ref('Chat-Plus-Admin')
|
||||
const logo = ref('/images/logo.png')
|
||||
const version = ref(process.env.VUE_APP_VERSION)
|
||||
|
||||
// 加载系统配置
|
||||
httpGet('/api/admin/config/get?key=system').then(res => {
|
||||
title.value = res.data['admin_title'];
|
||||
title.value = res.data['admin_title']
|
||||
logo.value = res.data['logo']
|
||||
}).catch(e => {
|
||||
ElMessage.error("加载系统配置失败: " + e.message)
|
||||
})
|
||||
@@ -192,9 +192,9 @@ setMenuItems(items)
|
||||
padding 6px 15px;
|
||||
|
||||
.el-image {
|
||||
width 30px;
|
||||
height 30px;
|
||||
padding-top 8px;
|
||||
width 36px;
|
||||
height 36px;
|
||||
padding-top 5px;
|
||||
border-radius 100%
|
||||
|
||||
.el-image__inner {
|
||||
|
||||
@@ -2,8 +2,14 @@ import {createRouter, createWebHistory} from "vue-router";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
name: 'home',
|
||||
name: 'Index',
|
||||
path: '/',
|
||||
meta: {title: process.env.VUE_APP_TITLE},
|
||||
component: () => import('@/views/Index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'home',
|
||||
path: '/home',
|
||||
redirect: '/chat',
|
||||
meta: {title: '首页'},
|
||||
component: () => import('@/views/Home.vue'),
|
||||
@@ -56,6 +62,18 @@ const routes = [
|
||||
meta: {title: '消费日志'},
|
||||
component: () => import('@/views/PowerLog.vue'),
|
||||
},
|
||||
{
|
||||
name: 'xmind',
|
||||
path: '/xmind',
|
||||
meta: {title: '思维导图'},
|
||||
component: () => import('@/views/MarkMap.vue'),
|
||||
},
|
||||
{
|
||||
name: 'dalle',
|
||||
path: '/dalle',
|
||||
meta: {title: 'DALLE-3'},
|
||||
component: () => import('@/views/Dalle.vue'),
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
import {getAdminToken, getSessionId, getUserToken} from "@/store/session";
|
||||
|
||||
axios.defaults.timeout = 30000
|
||||
axios.defaults.timeout = 180000
|
||||
axios.defaults.baseURL = process.env.VUE_APP_API_HOST
|
||||
axios.defaults.withCredentials = true;
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/json'
|
||||
|
||||
@@ -12,14 +12,10 @@
|
||||
<div class="title">
|
||||
<span class="name">{{ scope.item.name }}</span>
|
||||
<div class="opt">
|
||||
|
||||
<el-button v-if="hasRole(scope.item.key)" size="small" type="danger"
|
||||
@click="updateRole(scope.item,'remove')">
|
||||
<el-icon>
|
||||
<Delete/>
|
||||
</el-icon>
|
||||
<span>移除应用</span>
|
||||
</el-button>
|
||||
<div v-if="hasRole(scope.item.key)">
|
||||
<el-button size="small" type="success" @click="useRole(scope.item.id)">使用</el-button>
|
||||
<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')">
|
||||
@@ -47,10 +43,11 @@ import {onMounted, ref} from "vue"
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import ItemList from "@/components/ItemList.vue";
|
||||
import {Delete, Plus} from "@element-plus/icons-vue";
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
import {checkSession} from "@/action/session";
|
||||
import {arrayContains, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const listBoxHeight = window.innerHeight - 97
|
||||
const list = ref([])
|
||||
@@ -111,6 +108,11 @@ const updateRole = (row, opt) => {
|
||||
const hasRole = (roleKey) => {
|
||||
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const useRole = (roleId) => {
|
||||
router.push({name: "chat", params: {role_id: roleId}})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
@@ -82,8 +82,8 @@
|
||||
<el-main v-loading="loading" element-loading-background="rgba(122, 122, 122, 0.3)">
|
||||
<div class="chat-head">
|
||||
<div class="chat-config">
|
||||
<!-- <span class="role-select-label">聊天角色:</span>-->
|
||||
<el-select v-model="roleId" filterable placeholder="角色" class="role-select" @change="_newChat">
|
||||
<el-select v-model="roleId" filterable placeholder="角色" class="role-select" @change="_newChat"
|
||||
style="width:150px">
|
||||
<el-option
|
||||
v-for="item in roles"
|
||||
:key="item.id"
|
||||
@@ -97,7 +97,8 @@
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<el-select v-model="modelID" placeholder="模型" @change="_newChat">
|
||||
<el-select v-model="modelID" placeholder="模型" @change="_newChat" :disabled="disableModel"
|
||||
style="width:150px">
|
||||
<el-option
|
||||
v-for="item in models"
|
||||
:key="item.id"
|
||||
@@ -122,28 +123,6 @@
|
||||
<i class="iconfont icon-export"></i>
|
||||
<span>导出会话</span>
|
||||
</el-button>
|
||||
|
||||
<el-tooltip class="box-item"
|
||||
effect="dark"
|
||||
content="部署文档"
|
||||
placement="bottom">
|
||||
<a href="https://ai.r9it.com/docs/install/" target="_blank">
|
||||
<el-button type="primary" circle>
|
||||
<i class="iconfont icon-book"></i>
|
||||
</el-button>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="box-item"
|
||||
effect="dark"
|
||||
content="项目源码"
|
||||
placement="bottom">
|
||||
<a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
|
||||
<el-button type="success" circle>
|
||||
<i class="iconfont icon-github"></i>
|
||||
</el-button>
|
||||
</a>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -294,9 +273,9 @@ const leftBoxHeight = ref(0);
|
||||
const loading = ref(true);
|
||||
const loginUser = ref(null);
|
||||
const roles = ref([]);
|
||||
const router = useRouter();
|
||||
const roleId = ref(0)
|
||||
const newChatItem = ref(null);
|
||||
const router = useRouter();
|
||||
const showConfigDialog = ref(false);
|
||||
const showLoginDialog = ref(false)
|
||||
const isLogin = ref(false)
|
||||
@@ -327,6 +306,7 @@ httpGet("/api/config/get?key=notice").then(res => {
|
||||
showNotice.value = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
|
||||
}).catch(e => {
|
||||
@@ -375,17 +355,14 @@ const initData = () => {
|
||||
// 加载角色列表
|
||||
httpGet(`/api/role/list`).then((res) => {
|
||||
roles.value = res.data;
|
||||
roleId.value = roles.value[0]['id'];
|
||||
|
||||
const chatId = localStorage.getItem("chat_id")
|
||||
const chat = getChatById(chatId)
|
||||
if (chat === null) {
|
||||
// 创建新的对话
|
||||
newChat();
|
||||
console.log()
|
||||
if (router.currentRoute.value.params.role_id) {
|
||||
roleId.value = parseInt(router.currentRoute.value.params["role_id"])
|
||||
} else {
|
||||
// 加载对话
|
||||
loadChat(chat)
|
||||
roleId.value = roles.value[0]['id']
|
||||
}
|
||||
|
||||
newChat();
|
||||
}).catch((e) => {
|
||||
ElMessage.error('获取聊天角色失败: ' + e.messages)
|
||||
})
|
||||
@@ -445,6 +422,8 @@ const _newChat = () => {
|
||||
newChat()
|
||||
}
|
||||
}
|
||||
|
||||
const disableModel = ref(false)
|
||||
// 新建会话
|
||||
const newChat = () => {
|
||||
if (!isLogin.value) {
|
||||
@@ -452,10 +431,11 @@ const newChat = () => {
|
||||
return;
|
||||
}
|
||||
const role = getRoleById(roleId.value)
|
||||
if (role.key === 'gpt') {
|
||||
showHello.value = true
|
||||
} else {
|
||||
showHello.value = false
|
||||
showHello.value = role.key === 'gpt';
|
||||
// if the role bind a model, disable model change
|
||||
if (role.model_id > 0) {
|
||||
modelID.value = role.model_id
|
||||
disableModel.value = true
|
||||
}
|
||||
// 已有新开的会话
|
||||
if (newChatItem.value !== null && newChatItem.value['role_id'] === roles.value[0]['role_id']) {
|
||||
@@ -678,6 +658,7 @@ const connect = function (chat_id, role_id) {
|
||||
reader.onload = () => {
|
||||
const data = JSON.parse(String(reader.result));
|
||||
if (data.type === 'start') {
|
||||
console.log(data)
|
||||
chatData.value.push({
|
||||
type: "reply",
|
||||
id: randString(32),
|
||||
|
||||
452
web/src/views/Dalle.vue
Normal file
@@ -0,0 +1,452 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-dall">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="sd-box">
|
||||
<h2>DALL-E 创作中心</h2>
|
||||
|
||||
<div class="sd-params" :style="{ height: paramBoxHeight + 'px' }">
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
<div class="param-line" style="padding-top: 10px">
|
||||
<el-form-item label="图片质量">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.quality" style="width:176px">
|
||||
<el-option v-for="v in qualities" :label="v.name" :value="v.value" :key="v.value"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片尺寸">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.size" style="width:176px">
|
||||
<el-option v-for="v in sizes" :label="v" :value="v" :key="v"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片样式">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.style" style="width:176px">
|
||||
<el-option v-for="v in styles" :label="v.name" :value="v.value" :key="v.value"/>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="生动使模型倾向于生成超真实和戏剧性的图像"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="params.prompt"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
type="textarea"
|
||||
ref="promptRef"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-info">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-tag>每次绘图消耗{{ dallPower }}算力</el-tag>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-tag type="success">当前可用{{ power }}算力</el-tag>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="submit-btn">
|
||||
<el-button color="#47fff1" :dark="false" round @click="generate">
|
||||
立即生成
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-list-box" @scrollend="handleScrollEnd">
|
||||
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
|
||||
<div class="job-list-box">
|
||||
<h2>任务列表</h2>
|
||||
<div class="running-job-list">
|
||||
<ItemList :items="runningJobs" v-if="runningJobs.length > 0" :width="240">
|
||||
<template #default>
|
||||
<div class="job-item">
|
||||
<el-image fit="cover">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<i class="iconfont icon-quick-start"></i>
|
||||
<span>任务正在排队中</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</template>
|
||||
</ItemList>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div>
|
||||
|
||||
<h2>创作记录</h2>
|
||||
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.5)">
|
||||
<div v-if="finishedJobs.length > 0">
|
||||
<ItemList :items="finishedJobs" :width="240" :gap="16">
|
||||
<template #default="scope">
|
||||
<div class="job-item">
|
||||
<el-image v-if="scope.item['img_url']"
|
||||
:src="scope.item['img_url']+'?imageView2/1/w/240/h/240/q/75'"
|
||||
fit="cover"
|
||||
:preview-src-list="[scope.item['img_url']]"
|
||||
loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<el-image v-else
|
||||
:src="scope.item['org_url']"
|
||||
fit="cover"
|
||||
:preview-src-list="[scope.item['org_url']]"
|
||||
loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-tooltip content="删除" placement="top" effect="light">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage($event,scope.item)" circle/>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="分享" placement="top" effect="light" v-if="scope.item.publish">
|
||||
<el-button type="warning"
|
||||
@click="publishImage($event,scope.item, false)"
|
||||
circle>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="取消分享" placement="top" effect="light" v-else>
|
||||
<el-button type="success" @click="publishImage($event,scope.item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="复制提示词" placement="top" effect="light">
|
||||
<el-button type="info" circle class="copy-prompt" :data-clipboard-text="scope.item.prompt">
|
||||
<i class="iconfont icon-file"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ItemList>
|
||||
|
||||
<div class="no-more-data" v-if="isOver">
|
||||
<span>没有更多数据了</span>
|
||||
<i class="iconfont icon-face"></i>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" v-else/>
|
||||
</div> <!-- end finish job list-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end task list box -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<login-dialog :show="showLoginDialog" @hide="showLoginDialog = false" @success="initData"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, onUnmounted, ref} from "vue"
|
||||
import {Delete, InfoFilled, Picture} from "@element-plus/icons-vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
|
||||
import ItemList from "@/components/ItemList.vue";
|
||||
import Clipboard from "clipboard";
|
||||
import {checkSession} from "@/action/session";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const paramBoxHeight = ref(window.innerHeight - 150)
|
||||
const showLoginDialog = ref(false)
|
||||
const isLogin = ref(false)
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
paramBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
const qualities = [
|
||||
{name: "标准", value: "standard"},
|
||||
{name: "高清", value: "hd"},
|
||||
]
|
||||
const sizes = ["1024x1024", "1792x1024", "1024x1792"]
|
||||
const styles = [
|
||||
{name: "生动", value: "vivid"},
|
||||
{name: "自然", value: "natural"}
|
||||
]
|
||||
const params = ref({
|
||||
quality: "standard",
|
||||
size: "1024x1024",
|
||||
style: "vivid",
|
||||
prompt: ""
|
||||
})
|
||||
|
||||
const finishedJobs = ref([])
|
||||
const runningJobs = ref([])
|
||||
const power = ref(0)
|
||||
const dallPower = ref(0) // 画一张 SD 图片消耗算力
|
||||
const clipboard = ref(null)
|
||||
const userId = ref(0)
|
||||
onMounted(() => {
|
||||
initData()
|
||||
clipboard.value = new Clipboard('.copy-prompt');
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success("复制成功!");
|
||||
})
|
||||
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
dallPower.value = res.data["dall_power"]
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + 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 handleScrollEnd = () => {
|
||||
if (isOver.value === true) {
|
||||
return
|
||||
}
|
||||
page.value += 1
|
||||
fetchFinishJobs(page.value)
|
||||
}
|
||||
|
||||
const socket = ref(null)
|
||||
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) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8")
|
||||
reader.onload = () => {
|
||||
const message = String(reader.result)
|
||||
if (message === "FINISH") {
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
isOver.value = false
|
||||
}
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_socket.addEventListener('close', () => {
|
||||
if (socket.value !== null) {
|
||||
connect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
ElNotification({
|
||||
title: '任务执行失败',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: `任务ID:${jobs[i]['task_id']}<br />原因:${jobs[i]['err_msg']}`,
|
||||
type: 'error',
|
||||
})
|
||||
power.value += dallPower.value
|
||||
continue
|
||||
}
|
||||
_jobs.push(jobs[i])
|
||||
}
|
||||
runningJobs.value = _jobs
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取任务失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
const isOver = ref(false)
|
||||
const loading = ref(false)
|
||||
// 获取已完成的任务
|
||||
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) {
|
||||
isOver.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
|
||||
ElMessage.error("获取任务失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 创建绘图任务
|
||||
const promptRef = ref(null)
|
||||
const generate = () => {
|
||||
if (params.value.prompt === '') {
|
||||
promptRef.value.focus()
|
||||
return ElMessage.error("请输入绘画提示词!")
|
||||
}
|
||||
|
||||
if (!isLogin.value) {
|
||||
showLoginDialog.value = true
|
||||
return
|
||||
}
|
||||
httpPost("/api/dall/image", params.value).then(() => {
|
||||
ElMessage.success("任务执行成功!")
|
||||
power.value -= dallPower.value
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务执行失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeImage = (event, item) => {
|
||||
event.stopPropagation()
|
||||
ElMessageBox.confirm(
|
||||
'此操作将会删除任务和图片,继续操作码?',
|
||||
'删除提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(() => {
|
||||
httpPost("/api/dall/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
|
||||
ElMessage.success("任务删除成功")
|
||||
fetchFinishJobs(1)
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务删除失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (event, item, action) => {
|
||||
event.stopPropagation()
|
||||
let text = "图片发布"
|
||||
if (action === false) {
|
||||
text = "取消发布"
|
||||
}
|
||||
httpPost("/api/dall/publish", {id: item.id, action: action}).then(() => {
|
||||
ElMessage.success(text + "成功")
|
||||
item.publish = action
|
||||
}).catch(e => {
|
||||
ElMessage.error(text + "失败:" + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/image-dall.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
||||
@@ -2,16 +2,40 @@
|
||||
<div class="home">
|
||||
<div class="navigator">
|
||||
<div class="logo">
|
||||
<el-image :src="logo"/>
|
||||
<el-image :src="logo" @click="router.push('/')"/>
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<ul class="nav-items">
|
||||
<li v-for="item in navs" :key="item.url">
|
||||
<li v-for="item in mainNavs" :key="item.url">
|
||||
<a @click="changeNav(item)" :class="item.url === curPath ? 'active' : ''">
|
||||
<el-image :src="item.icon" style="width: 30px;height: 30px"/>
|
||||
</a>
|
||||
<div :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</div>
|
||||
</li>
|
||||
|
||||
<el-popover
|
||||
v-if="moreNavs.length > 0"
|
||||
placement="right-end"
|
||||
trigger="hover"
|
||||
>
|
||||
<template #reference>
|
||||
<li>
|
||||
<a class="active">
|
||||
<el-image src="/images/menu/more.png" style="width: 30px;height: 30px"/>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
<template #default>
|
||||
<ul class="more-menus">
|
||||
<li v-for="item in moreNavs" :key="item.url" :class="item.url === curPath ? 'active' : ''">
|
||||
<a @click="changeNav(item)">
|
||||
<el-image :src="item.icon" style="width: 20px;height: 20px"/>
|
||||
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</el-popover>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content">
|
||||
@@ -33,7 +57,8 @@ import {ElMessage} from "element-plus";
|
||||
|
||||
const router = useRouter();
|
||||
const logo = ref('/images/logo.png');
|
||||
const navs = ref([])
|
||||
const mainNavs = ref([])
|
||||
const moreNavs = ref([])
|
||||
const curPath = ref(router.currentRoute.value.path)
|
||||
|
||||
const changeNav = (item) => {
|
||||
@@ -49,7 +74,11 @@ onMounted(() => {
|
||||
})
|
||||
// 获取菜单
|
||||
httpGet("/api/menu/list").then(res => {
|
||||
navs.value = res.data
|
||||
mainNavs.value = res.data
|
||||
if (res.data.length > 7) {
|
||||
mainNavs.value = res.data.slice(0, 7)
|
||||
moreNavs.value = res.data.slice(7)
|
||||
}
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统菜单失败:" + e.message)
|
||||
})
|
||||
@@ -75,6 +104,7 @@ onMounted(() => {
|
||||
display flex
|
||||
flex-flow column
|
||||
align-items center
|
||||
cursor pointer
|
||||
|
||||
.el-image {
|
||||
width 50px
|
||||
@@ -89,7 +119,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
margin-top: 20px;
|
||||
margin-top: 10px;
|
||||
padding 0 5px
|
||||
|
||||
li {
|
||||
@@ -131,13 +161,40 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
width: 100%
|
||||
height: 100vh
|
||||
box-sizing: border-box
|
||||
background-color #282c34
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.el-popper {
|
||||
.more-menus {
|
||||
li {
|
||||
padding 10px 15px
|
||||
cursor pointer
|
||||
border-radius 5px
|
||||
margin 5px 0
|
||||
|
||||
.el-image {
|
||||
position: relative
|
||||
top 5px
|
||||
right 5px
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color #f1f1f1
|
||||
}
|
||||
}
|
||||
|
||||
li.active {
|
||||
background-color #f1f1f1
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="mj-box">
|
||||
<h2>MidJourney 创作中心</h2>
|
||||
|
||||
<div class="mj-params" :style="{ height: mjBoxHeight + 'px' }">
|
||||
<div class="mj-params" :style="{ height: paramBoxHeight + 'px' }">
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
<div class="param-line pt">
|
||||
<span>图片比例:</span>
|
||||
@@ -33,7 +33,7 @@
|
||||
<el-form-item label="图片画质">
|
||||
<template #default>
|
||||
<div class="form-item-inner flex-row items-center">
|
||||
<el-select v-model="params.quality" placeholder="请选择">
|
||||
<el-select v-model="params.quality" placeholder="请选择" style="width:175px">
|
||||
<el-option v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
@@ -525,42 +525,10 @@
|
||||
<div class="opt" v-if="scope.item['can_opt']">
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="放大第一张"
|
||||
placement="top">
|
||||
<a @click="upscale(1, scope.item)">U1</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="放大第二张"
|
||||
placement="top">
|
||||
<a @click="upscale(2, scope.item)">U2</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="放大第三张"
|
||||
placement="top">
|
||||
<a @click="upscale(3, scope.item)">U3</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="放大第四张"
|
||||
placement="top">
|
||||
<a @click="upscale(4, scope.item)">U4</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li><a @click="upscale(1, scope.item)">U1</a></li>
|
||||
<li><a @click="upscale(2, scope.item)">U2</a></li>
|
||||
<li><a @click="upscale(3, scope.item)">U3</a></li>
|
||||
<li><a @click="upscale(4, scope.item)">U4</a></li>
|
||||
<li class="show-prompt">
|
||||
|
||||
<el-popover placement="left" title="提示词" :width="240" trigger="hover">
|
||||
@@ -586,42 +554,10 @@
|
||||
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="变化第一张"
|
||||
placement="top">
|
||||
<a @click="variation(1, scope.item)">V1</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="变化第二张"
|
||||
placement="top">
|
||||
<a @click="variation(2, scope.item)">V2</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="变化第三张"
|
||||
placement="top">
|
||||
<a @click="variation(3, scope.item)">V3</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="变化第四张"
|
||||
placement="top">
|
||||
<a @click="variation(4, scope.item)">V4</a>
|
||||
</el-tooltip>
|
||||
</li>
|
||||
<li><a @click="variation(1, scope.item)">V1</a></li>
|
||||
<li><a @click="variation(2, scope.item)">V2</a></li>
|
||||
<li><a @click="variation(3, scope.item)">V3</a></li>
|
||||
<li><a @click="variation(4, scope.item)">V4</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -671,12 +607,12 @@ import {copyObj, removeArrayItem} from "@/utils/libs";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const paramBoxHeight = ref(window.innerHeight - 150)
|
||||
const showLoginDialog = ref(false)
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
paramBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
const rates = [
|
||||
{css: "square", value: "1:1", text: "1:1", img: "/images/mj/rate_1_1.png"},
|
||||
@@ -789,10 +725,17 @@ const connect = () => {
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
fetchRunningJobs()
|
||||
isOver.value = false
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8")
|
||||
reader.onload = () => {
|
||||
const message = String(reader.result)
|
||||
if (message === "FINISH") {
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
isOver.value = false
|
||||
}
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -994,8 +937,6 @@ const generate = () => {
|
||||
httpPost("/api/mj/image", params.value).then(() => {
|
||||
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...")
|
||||
power.value -= mjPower.value
|
||||
params.value = copyObj(initParams)
|
||||
imgList.value = []
|
||||
}).catch(e => {
|
||||
ElMessage.error("任务推送失败:" + e.message)
|
||||
})
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
<div class="sd-box">
|
||||
<h2>Stable Diffusion 创作中心</h2>
|
||||
|
||||
<div class="sd-params" :style="{ height: mjBoxHeight + 'px' }">
|
||||
<div class="sd-params" :style="{ height: paramBoxHeight + 'px' }">
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
<div class="param-line" style="padding-top: 10px">
|
||||
<el-form-item label="采样方法">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.sampler" size="small">
|
||||
<el-select v-model="params.sampler" style="width:176px">
|
||||
<el-option v-for="item in samplers" :label="item" :value="item" :key="item"/>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
@@ -20,7 +20,7 @@
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -35,10 +35,10 @@
|
||||
<div class="form-item-inner">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-input v-model.number="params.width" size="small" placeholder="图片宽度"/>
|
||||
<el-input v-model.number="params.width" placeholder="图片宽度"/>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-input v-model.number="params.height" size="small" placeholder="图片高度"/>
|
||||
<el-input v-model.number="params.height" placeholder="图片高度"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
@@ -50,14 +50,14 @@
|
||||
<el-form-item label="迭代步数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.steps" size="small"/>
|
||||
<el-input v-model.number="params.steps"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="值越大则代表细节越多,同时也意味着出图速度越慢"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -70,14 +70,14 @@
|
||||
<el-form-item label="引导系数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.cfg_scale" size="small"/>
|
||||
<el-input v-model.number="params.cfg_scale"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -90,14 +90,14 @@
|
||||
<el-form-item label="随机因子">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.seed" size="small"/>
|
||||
<el-input v-model.number="params.seed"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -108,7 +108,7 @@
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon @click="params.seed = -1">
|
||||
<el-icon @click="params.seed = -1" class="info-icon">
|
||||
<Orange/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -121,14 +121,14 @@
|
||||
<el-form-item label="高清修复">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1;"/>
|
||||
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1;" size="large"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon style="margin-top: 6px">
|
||||
<el-icon style="margin-left: 10px; top: 12px">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -150,7 +150,7 @@
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon style="margin-top: 6px">
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -163,7 +163,7 @@
|
||||
<el-form-item label="放大算法">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.hd_scale_alg" size="small">
|
||||
<el-select v-model="params.hd_scale_alg" style="width:176px">
|
||||
<el-option v-for="item in scaleAlg" :label="item" :value="item" :key="item"/>
|
||||
</el-select>
|
||||
<el-tooltip
|
||||
@@ -172,7 +172,7 @@
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -185,14 +185,14 @@
|
||||
<el-form-item label="放大倍数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.hd_scale" size="small"/>
|
||||
<el-input v-model.number="params.hd_scale"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -205,14 +205,14 @@
|
||||
<el-form-item label="迭代步数">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.hd_steps" size="small"/>
|
||||
<el-input v-model.number="params.hd_steps"/>
|
||||
<el-tooltip
|
||||
effect="light"
|
||||
content="重绘迭代步数,如果设置为0,则设置跟原图相同的迭代步数"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -239,7 +239,7 @@
|
||||
content="不希望出现的元素,下面给了默认的起手式"
|
||||
placement="right"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
@@ -254,8 +254,14 @@
|
||||
</div>
|
||||
|
||||
<div class="text-info">
|
||||
<el-tag>每次绘图消耗{{ sdPower }}算力</el-tag>
|
||||
<el-tag type="success">当前可用算力:{{ power }}</el-tag>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-tag>单次绘图消耗{{ sdPower }}算力</el-tag>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-tag type="success">当前可用{{ power }}算力</el-tag>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
</el-form>
|
||||
@@ -492,7 +498,7 @@ import {getSessionId} from "@/store/session";
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const paramBoxHeight = ref(window.innerHeight - 150)
|
||||
const fullImgHeight = ref(window.innerHeight - 60)
|
||||
const showTaskDialog = ref(false)
|
||||
const item = ref({})
|
||||
@@ -501,7 +507,7 @@ const isLogin = ref(false)
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
paramBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
const samplers = ["Euler a", "DPM++ 2S a Karras", "DPM++ 2M Karras", "DPM++ SDE Karras", "DPM++ 2M SDE Karras"]
|
||||
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"]
|
||||
@@ -568,10 +574,17 @@ const connect = () => {
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
fetchRunningJobs()
|
||||
isOver.value = false
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8")
|
||||
reader.onload = () => {
|
||||
const message = String(reader.result)
|
||||
if (message === "FINISH") {
|
||||
page.value = 1
|
||||
fetchFinishJobs(page.value)
|
||||
isOver.value = false
|
||||
}
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -579,7 +592,7 @@ const connect = () => {
|
||||
if (socket.value !== null) {
|
||||
connect()
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const clipboard = ref(null)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<el-radio-group v-model="imgType" @change="changeImgType">
|
||||
<el-radio label="mj" size="large">MidJourney</el-radio>
|
||||
<el-radio label="sd" size="large">Stable Diffusion</el-radio>
|
||||
<el-radio label="dall" size="large">DALL-E</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,6 +72,57 @@
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
|
||||
<v3-waterfall v-if="imgType === 'dall'"
|
||||
id="waterfall"
|
||||
:list="data['dall']"
|
||||
srcKey="img_thumb"
|
||||
:gap="12"
|
||||
:bottomGap="-5"
|
||||
:colWidth="colWidth"
|
||||
:distanceToScroll="100"
|
||||
:isLoading="loading"
|
||||
:isOver="false"
|
||||
@scrollReachBottom="getNext">
|
||||
<template #default="slotProp">
|
||||
<div class="list-item">
|
||||
<div class="image">
|
||||
<el-image :src="slotProp.item['img_thumb']"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[slotProp.item['img_url']]"
|
||||
:preview-teleported="true"
|
||||
:initial-index="10"
|
||||
loading="lazy">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">
|
||||
正在加载图片
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="opt">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="light"
|
||||
content="复制提示词"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
|
||||
<v3-waterfall v-else
|
||||
id="waterfall"
|
||||
:list="data['sd']"
|
||||
@@ -252,7 +304,8 @@ import {useRouter} from "vue-router";
|
||||
|
||||
const data = ref({
|
||||
"mj": [],
|
||||
"sd": []
|
||||
"sd": [],
|
||||
"dall": [],
|
||||
})
|
||||
const loading = ref(true)
|
||||
const isOver = ref(false)
|
||||
@@ -284,10 +337,22 @@ const getNext = () => {
|
||||
|
||||
loading.value = true
|
||||
page.value = page.value + 1
|
||||
const url = imgType.value === "mj" ? "/api/mj/imgWall" : "/api/sd/imgWall"
|
||||
let url = ""
|
||||
console.log(imgType.value)
|
||||
switch (imgType.value) {
|
||||
case "mj":
|
||||
url = "/api/mj/imgWall"
|
||||
break
|
||||
case "sd":
|
||||
url = "/api/sd/imgWall"
|
||||
break
|
||||
case "dall":
|
||||
url = "/api/dall/imgWall"
|
||||
break
|
||||
}
|
||||
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`).then(res => {
|
||||
loading.value = false
|
||||
if (res.data.length === 0) {
|
||||
if (!res.data || res.data.length === 0) {
|
||||
isOver.value = true
|
||||
return
|
||||
}
|
||||
@@ -335,7 +400,8 @@ const changeImgType = () => {
|
||||
page.value = 0
|
||||
data.value = {
|
||||
"mj": [],
|
||||
"sd": []
|
||||
"sd": [],
|
||||
"dall": [],
|
||||
}
|
||||
loading.value = true
|
||||
isOver.value = false
|
||||
|
||||
237
web/src/views/Index.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="index-page" :style="{height: winHeight+'px'}">
|
||||
<div class="menu-box">
|
||||
<el-menu
|
||||
mode="horizontal"
|
||||
:ellipsis="false"
|
||||
>
|
||||
<div class="menu-item">
|
||||
<el-image :src="logo" alt="Geek-AI"/>
|
||||
<div class="title">{{ title }}</div>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<a href="https://ai.r9it.com/docs/install/" target="_blank">
|
||||
<el-button type="primary" round>
|
||||
<i class="iconfont icon-book"></i>
|
||||
<span>部署文档</span>
|
||||
</el-button>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
|
||||
<el-button type="success" round>
|
||||
<i class="iconfont icon-github"></i>
|
||||
<span>项目源码</span>
|
||||
</el-button>
|
||||
</a>
|
||||
</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>AI-MJ绘画</span>
|
||||
</el-button>
|
||||
|
||||
<el-button @click="router.push('/sd')" color="#4AE6DF" style="color:#424282" :dark="false">
|
||||
<i class="iconfont icon-sd"></i>
|
||||
<span>AI-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>
|
||||
|
||||
<div class="footer">
|
||||
<footer-bar />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import * as THREE from 'three';
|
||||
import {onMounted, ref} from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import FooterBar from "@/components/FooterBar.vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const title = ref("Geek-AI 创作系统")
|
||||
const logo = ref("/images/logo.png")
|
||||
const slogan = ref("我辈之人,先干为敬,陪您先把 AI 用起来")
|
||||
const size = Math.max(window.innerWidth * 0.5, window.innerHeight * 0.8)
|
||||
const winHeight = window.innerHeight - 150
|
||||
|
||||
onMounted(() => {
|
||||
httpGet("/api/config/get?key=system").then(res => {
|
||||
title.value = res.data.title
|
||||
logo.value = res.data.logo
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + 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('#007bff'), // 高光颜色
|
||||
});
|
||||
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>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import '@/assets/iconfont/iconfont.css'
|
||||
.index-page {
|
||||
margin: 0
|
||||
background-color #007bff /* 科技蓝色背景 */
|
||||
overflow hidden
|
||||
color #ffffff
|
||||
display flex
|
||||
justify-content center
|
||||
align-items baseline
|
||||
padding-top 150px
|
||||
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
331
web/src/views/MarkMap.vue
Normal file
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-mark-map">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="mark-map-box">
|
||||
<h2>思维导图创作中心</h2>
|
||||
|
||||
<div class="mark-map-params" :style="{ height: leftBoxHeight + 'px' }">
|
||||
<el-form label-width="80px" label-position="left">
|
||||
<div class="param-line">
|
||||
你的需求?
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
type="textarea"
|
||||
placeholder="请给AI输入提示词,让AI帮你完善"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
请选择生成思维导图的AI模型
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-select v-model="modelID" placeholder="请选择模型" @change="changeModel" style="width:100%">
|
||||
<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 class="text-info">
|
||||
<el-tag type="success">当前可用算力:{{ loginUser.power }}</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-button color="#47fff1" :dark="false" round @click="generateAI" :loading="loading">
|
||||
智能生成思维导图
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
使用已有内容生成?
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input
|
||||
v-model="content"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
type="textarea"
|
||||
placeholder="请用markdown语法输入您想要生成思维导图的内容!"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-button color="#C5F9AE" :dark="false" round @click="generate">直接生成(免费)</el-button>
|
||||
</div>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-box">
|
||||
<div class="top-bar">
|
||||
<h2>思维导图</h2>
|
||||
<el-button @click="downloadImage" type="primary">
|
||||
<el-icon>
|
||||
<Download/>
|
||||
</el-icon>
|
||||
<span>下载图片</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="markdown" v-if="loading">
|
||||
<div v-html="html"></div>
|
||||
</div>
|
||||
<div class="body" id="markmap" v-show="!loading">
|
||||
<svg ref="svgRef" :style="{ height: rightBoxHeight + 'px' }"/>
|
||||
</div>
|
||||
</div><!-- end task list box -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<login-dialog :show="showLoginDialog" @hide="showLoginDialog = false" @success="initData"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LoginDialog from "@/components/LoginDialog.vue";
|
||||
import {nextTick, onMounted, onUnmounted, ref} from 'vue';
|
||||
import {Markmap} from 'markmap-view';
|
||||
import {loadCSS, loadJS} from 'markmap-common';
|
||||
import {Transformer} from 'markmap-lib';
|
||||
import {checkSession} from "@/action/session";
|
||||
import {httpGet} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {Download} from "@element-plus/icons-vue";
|
||||
|
||||
const leftBoxHeight = ref(window.innerHeight - 105)
|
||||
const rightBoxHeight = ref(window.innerHeight - 85)
|
||||
|
||||
const prompt = ref("")
|
||||
const text = ref(`# Geek-AI 助手
|
||||
|
||||
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||||
- 基于 Websocket 实现,完美的打字机体验。
|
||||
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求。
|
||||
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||||
- 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
||||
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||||
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
|
||||
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件。
|
||||
`)
|
||||
const md = require('markdown-it')({breaks: true});
|
||||
const content = ref(text.value)
|
||||
const html = ref("")
|
||||
|
||||
const showLoginDialog = ref(false)
|
||||
const isLogin = ref(false)
|
||||
const loginUser = ref({power: 0})
|
||||
const transformer = new Transformer();
|
||||
const {scripts, styles} = transformer.getAssets()
|
||||
loadCSS(styles);
|
||||
loadJS(scripts);
|
||||
|
||||
|
||||
const svgRef = ref(null)
|
||||
const markMap = ref(null)
|
||||
const models = ref([])
|
||||
const modelID = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
initData()
|
||||
markMap.value = Markmap.create(svgRef.value)
|
||||
update()
|
||||
});
|
||||
|
||||
const initData = () => {
|
||||
checkSession().then(user => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
|
||||
httpGet("/api/model/list").then(res => {
|
||||
for (let v of res.data) {
|
||||
if (v.platform === "OpenAI") {
|
||||
models.value.push(v)
|
||||
}
|
||||
}
|
||||
modelID.value = models.value[0].id
|
||||
connect(user.id)
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取模型失败:" + e.message)
|
||||
})
|
||||
}).catch(() => {
|
||||
});
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
|
||||
const {root} = transformer.transform(processContent(text.value))
|
||||
markMap.value.setData(root)
|
||||
markMap.value.fit()
|
||||
}
|
||||
|
||||
const processContent = (text) => {
|
||||
const arr = []
|
||||
const lines = text.split("\n")
|
||||
for (let line of lines) {
|
||||
if (line.indexOf("```") !== -1) {
|
||||
continue
|
||||
}
|
||||
line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
|
||||
arr.push(line)
|
||||
}
|
||||
return arr.join("\n")
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.close()
|
||||
}
|
||||
socket.value = null
|
||||
})
|
||||
|
||||
window.onresize = () => {
|
||||
leftBoxHeight.value = window.innerHeight - 145
|
||||
rightBoxHeight.value = window.innerHeight - 85
|
||||
}
|
||||
|
||||
const socket = ref(null)
|
||||
const heartbeatHandle = ref(null)
|
||||
const connect = (userId) => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.close()
|
||||
}
|
||||
|
||||
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/markMap/client?user_id=${userId}&model_id=${modelID.value}`);
|
||||
_socket.addEventListener('open', () => {
|
||||
socket.value = _socket;
|
||||
|
||||
// 发送心跳消息
|
||||
sendHeartbeat()
|
||||
});
|
||||
|
||||
_socket.addEventListener('message', event => {
|
||||
if (event.data instanceof Blob) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(event.data, "UTF-8")
|
||||
reader.onload = () => {
|
||||
const data = JSON.parse(String(reader.result))
|
||||
switch (data.type) {
|
||||
case "start":
|
||||
text.value = ""
|
||||
break
|
||||
case "middle":
|
||||
text.value += data.content
|
||||
html.value = md.render(processContent(text.value))
|
||||
break
|
||||
case "end":
|
||||
loading.value = false
|
||||
content.value = processContent(text.value)
|
||||
nextTick(() => update())
|
||||
break
|
||||
case "error":
|
||||
loading.value = false
|
||||
ElMessage.error(data.content)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
_socket.addEventListener('close', () => {
|
||||
loading.value = false
|
||||
if (socket.value !== null) {
|
||||
connect(userId)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const generate = () => {
|
||||
text.value = content.value
|
||||
update()
|
||||
}
|
||||
|
||||
// 使用 AI 智能生成
|
||||
const generateAI = () => {
|
||||
html.value = ''
|
||||
text.value = ''
|
||||
if (prompt.value === '') {
|
||||
return ElMessage.error("请输入你的需求")
|
||||
}
|
||||
if (!isLogin.value) {
|
||||
showLoginDialog.value = true
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
socket.value.send(JSON.stringify({type: "message", content: prompt.value}))
|
||||
}
|
||||
|
||||
const changeModel = () => {
|
||||
if (socket.value !== null) {
|
||||
socket.value.send(JSON.stringify({type: "model_id", content: modelID.value}))
|
||||
}
|
||||
}
|
||||
|
||||
// download SVG to png file
|
||||
const downloadImage = () => {
|
||||
const svgElement = document.getElementById("markmap");
|
||||
// 将 SVG 渲染到图片对象
|
||||
const serializer = new XMLSerializer()
|
||||
const source = '<?xml version="1.0" standalone="no"?>\r\n' + serializer.serializeToString(svgRef.value)
|
||||
const image = new Image()
|
||||
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source)
|
||||
|
||||
// 将图片对象渲染
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = svgElement.offsetWidth
|
||||
canvas.height = svgElement.offsetHeight
|
||||
let context = canvas.getContext('2d')
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
image.onload = function () {
|
||||
context.drawImage(image, 0, 0)
|
||||
const a = document.createElement('a')
|
||||
a.download = "geek-ai-xmind.png"
|
||||
a.href = canvas.toDataURL(`image/png`)
|
||||
a.click()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/mark-map.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
</style>
|
||||
@@ -19,7 +19,12 @@
|
||||
</div>
|
||||
|
||||
<el-row v-if="items.length > 0">
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border>
|
||||
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border
|
||||
style="--el-table-border-color:#373C47;
|
||||
--el-table-tr-bg-color:#2D323B;
|
||||
--el-table-row-hover-bg-color:#373C47;
|
||||
--el-table-header-bg-color:#474E5C;
|
||||
--el-table-text-color:#d1d1d1">
|
||||
<el-table-column prop="username" label="用户"/>
|
||||
<el-table-column prop="model" label="模型"/>
|
||||
<el-table-column prop="type" label="类型">
|
||||
@@ -64,7 +69,7 @@
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {Back, DocumentCopy, Search} from "@element-plus/icons-vue";
|
||||
import {Search} from "@element-plus/icons-vue";
|
||||
import Clipboard from "clipboard";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpPost} from "@/utils/http";
|
||||
@@ -79,7 +84,7 @@ const query = ref({
|
||||
model: "",
|
||||
date: []
|
||||
})
|
||||
const tagColors = ref(["", "success", "primary", "danger", "info", "warning"])
|
||||
const tagColors = ref(["", "success", "", "danger", "info", "warning"])
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
@@ -119,8 +124,7 @@ const fetchData = () => {
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.power-log {
|
||||
background-color #ffffff
|
||||
|
||||
color #ffffff
|
||||
.inner {
|
||||
padding 0 20px 20px 20px
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ const register = function () {
|
||||
align-items center
|
||||
|
||||
.contain {
|
||||
padding 0 40px 20px 40px;
|
||||
padding 20px 40px 20px 40px;
|
||||
width 100%
|
||||
color #ffffff
|
||||
border-radius 10px;
|
||||
|
||||
@@ -1,16 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
{{ title }}
|
||||
<textarea v-model="value"/>
|
||||
</div>
|
||||
<svg ref="svgRef"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted, onUpdated} from 'vue';
|
||||
import {Markmap} from 'markmap-view';
|
||||
import {loadJS, loadCSS} from 'markmap-common';
|
||||
import {Transformer} from 'markmap-lib';
|
||||
|
||||
import {ref} from "vue";
|
||||
const transformer = new Transformer();
|
||||
const {scripts, styles} = transformer.getAssets();
|
||||
loadCSS(styles);
|
||||
loadJS(scripts);
|
||||
|
||||
const title = ref('Test Page')
|
||||
const initValue = `# markmap
|
||||
|
||||
- beautiful
|
||||
- useful
|
||||
- easy
|
||||
- interactive
|
||||
`;
|
||||
|
||||
const value = ref(initValue);
|
||||
const svgRef = ref(null);
|
||||
let mm;
|
||||
|
||||
const update = () => {
|
||||
const {root} = transformer.transform(value.value);
|
||||
mm.setData(root);
|
||||
mm.fit();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
mm = Markmap.create(svgRef.value);
|
||||
update();
|
||||
});
|
||||
|
||||
onUpdated(update);
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
</style>
|
||||
@@ -125,8 +125,8 @@
|
||||
import {onMounted, onUnmounted, reactive, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, disabledDate, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {DocumentCopy, InfoFilled, Plus, ShoppingCart} from "@element-plus/icons-vue";
|
||||
import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {DocumentCopy, Plus, ShoppingCart} from "@element-plus/icons-vue";
|
||||
import ClipboardJS from "clipboard";
|
||||
|
||||
// 变量定义
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="container list" v-loading="loading">
|
||||
<div class="container model-list" v-loading="loading">
|
||||
|
||||
<div class="handle-box">
|
||||
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
|
||||
@@ -13,7 +13,14 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="模型名称"/>
|
||||
<el-table-column prop="value" label="模型值"/>
|
||||
<el-table-column prop="value" label="模型值">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.value }}</span>
|
||||
<el-icon class="copy-model" :data-clipboard-text="scope.row.value">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="power" label="费率"/>
|
||||
<el-table-column prop="max_tokens" label="最大响应长度"/>
|
||||
<el-table-column prop="max_context" label="最大上下文长度"/>
|
||||
@@ -29,12 +36,12 @@
|
||||
</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 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">
|
||||
<el-button size="small" type="primary" @click="edit(scope.row)">编辑</el-button>
|
||||
@@ -75,7 +82,7 @@
|
||||
<el-form-item label="费率:" prop="weight">
|
||||
<template #default>
|
||||
<div class="tip-input">
|
||||
<el-input-number :min="1" v-model="item.power" autocomplete="off"/>
|
||||
<el-input-number :min="0" v-model="item.power" autocomplete="off"/>
|
||||
<div class="info">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
@@ -144,6 +151,15 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="绑定API-KEY:" prop="apikey">
|
||||
<el-select v-model="item.key_id" placeholder="请选择 API KEY" clearable>
|
||||
<el-option v-for="v in apiKeys" :value="v.id" :label="v.name" :key="v.id">
|
||||
{{ v.name }}
|
||||
<el-text type="info" size="small">{{ substr(v.api_url, 50) }}</el-text>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态:" prop="enable">
|
||||
<el-switch v-model="item.enabled"/>
|
||||
</el-form-item>
|
||||
@@ -178,12 +194,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
import {onMounted, onUnmounted, reactive, ref} from "vue";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, removeArrayItem} from "@/utils/libs";
|
||||
import {InfoFilled, Plus} from "@element-plus/icons-vue";
|
||||
import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {DocumentCopy, InfoFilled, Plus} from "@element-plus/icons-vue";
|
||||
import {Sortable} from "sortablejs";
|
||||
import ClipboardJS from "clipboard";
|
||||
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
@@ -207,23 +224,34 @@ const platforms = ref([
|
||||
|
||||
])
|
||||
|
||||
// 获取数据
|
||||
httpGet('/api/admin/model/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)
|
||||
}
|
||||
items.value = arr
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
// 获取 API KEY
|
||||
const apiKeys = ref([])
|
||||
httpGet('/api/admin/apikey/list?status=true&type=chat').then(res => {
|
||||
apiKeys.value = res.data
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取 API KEY 失败:" + e.message)
|
||||
})
|
||||
|
||||
// 获取数据
|
||||
const fetchData = () => {
|
||||
httpGet('/api/admin/model/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)
|
||||
}
|
||||
items.value = arr
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取数据失败");
|
||||
})
|
||||
}
|
||||
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||
|
||||
// 初始化拖动排序插件
|
||||
@@ -250,6 +278,19 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
clipboard.value = new ClipboardJS('.copy-model');
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!');
|
||||
})
|
||||
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy()
|
||||
})
|
||||
|
||||
const add = function () {
|
||||
@@ -267,14 +308,15 @@ const edit = function (row) {
|
||||
const save = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
item.value.temperature = parseFloat(item.value.temperature)
|
||||
if (!item.value.sort_num) {
|
||||
item.value.sort_num = items.value.length
|
||||
}
|
||||
if (valid) {
|
||||
showDialog.value = false
|
||||
httpPost('/api/admin/model/save', item.value).then((res) => {
|
||||
item.value.key_id = parseInt(item.value.key_id)
|
||||
httpPost('/api/admin/model/save', item.value).then(() => {
|
||||
ElMessage.success('操作成功!')
|
||||
if (!item.value['id']) {
|
||||
const newItem = res.data
|
||||
items.value.push(newItem)
|
||||
}
|
||||
fetchData()
|
||||
}).catch((e) => {
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
@@ -306,7 +348,7 @@ const remove = function (row) {
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@import "@/assets/css/admin/form.styl";
|
||||
.list {
|
||||
.model-list {
|
||||
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
@@ -318,6 +360,13 @@ const remove = function (row) {
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
.copy-model {
|
||||
margin-left 6px
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ import {setAdminToken} from "@/store/session";
|
||||
import {checkAdminSession} from "@/action/session";
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref('Geek-AI 控制台登录');
|
||||
const title = ref('ChatGPT Plus Admin');
|
||||
const username = ref(process.env.VUE_APP_ADMIN_USER);
|
||||
const password = ref(process.env.VUE_APP_ADMIN_PASS);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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)"/>
|
||||
@@ -47,7 +48,7 @@
|
||||
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
title="编辑角色"
|
||||
:title="optTitle"
|
||||
:close-on-click-modal="false"
|
||||
width="50%"
|
||||
>
|
||||
@@ -67,10 +68,33 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色图标:" prop="icon">
|
||||
<el-input
|
||||
v-model="role.icon"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<el-input v-model="role.icon">
|
||||
<template #append>
|
||||
<el-upload
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="uploadImg"
|
||||
>
|
||||
上传
|
||||
</el-upload>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="绑定模型:" prop="model_id">
|
||||
<el-select
|
||||
v-model="role.model_id"
|
||||
filterable
|
||||
placeholder="请选择模型"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in models"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="打招呼信息:" prop="hello_msg">
|
||||
@@ -143,6 +167,7 @@ import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {copyObj, removeArrayItem} from "@/utils/libs";
|
||||
import {Sortable} from "sortablejs"
|
||||
import Compressor from "compressorjs";
|
||||
|
||||
const showDialog = ref(false)
|
||||
const parentBorder = ref(true)
|
||||
@@ -151,7 +176,7 @@ const tableData = ref([])
|
||||
const sortedTableData = ref([])
|
||||
const role = ref({context: []})
|
||||
const formRef = ref(null)
|
||||
const editRow = ref({})
|
||||
const optTitle = ref({})
|
||||
const loading = ref(true)
|
||||
|
||||
const rules = reactive({
|
||||
@@ -165,18 +190,30 @@ const rules = reactive({
|
||||
hello_msg: [{required: true, message: '请输入打招呼信息', trigger: 'change',}]
|
||||
})
|
||||
|
||||
// 获取角色列表
|
||||
httpGet('/api/admin/role/list').then((res) => {
|
||||
tableData.value = res.data
|
||||
sortedTableData.value = copyObj(tableData.value)
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取聊天角色失败");
|
||||
const models = ref([])
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
|
||||
// get chat models
|
||||
httpGet('/api/admin/model/list?enable=1').then((res) => {
|
||||
models.value = res.data
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取AI模型数据失败");
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||
const fetchData = () => {
|
||||
// 获取角色列表
|
||||
httpGet('/api/admin/role/list').then((res) => {
|
||||
tableData.value = res.data
|
||||
sortedTableData.value = copyObj(tableData.value)
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取聊天角色失败");
|
||||
})
|
||||
|
||||
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
|
||||
// 初始化拖动排序插件
|
||||
Sortable.create(drawBodyWrapper, {
|
||||
sort: true,
|
||||
@@ -199,7 +236,7 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const roleSet = (filed, row) => {
|
||||
httpPost('/api/admin/role/set', {id: row.id, filed: filed, value: row[filed]}).then(() => {
|
||||
@@ -212,12 +249,14 @@ const roleSet = (filed, row) => {
|
||||
// 编辑
|
||||
const curIndex = ref(0)
|
||||
const rowEdit = function (index, row) {
|
||||
optTitle.value = "修改角色"
|
||||
curIndex.value = index
|
||||
role.value = copyObj(row)
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const addRole = function () {
|
||||
optTitle.value = "添加新角色"
|
||||
role.value = {context: []}
|
||||
showDialog.value = true
|
||||
}
|
||||
@@ -226,14 +265,9 @@ const save = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
showDialog.value = false
|
||||
httpPost('/api/admin/role/save', role.value).then((res) => {
|
||||
httpPost('/api/admin/role/save', role.value).then(() => {
|
||||
ElMessage.success('操作成功')
|
||||
// 更新当前数据行
|
||||
if (role.value.id) {
|
||||
tableData.value[curIndex.value] = role.value
|
||||
} else {
|
||||
tableData.value.push(res.data)
|
||||
}
|
||||
fetchData()
|
||||
}).catch((e) => {
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
@@ -263,6 +297,27 @@ const removeContext = function (index) {
|
||||
role.value.context.splice(index, 1);
|
||||
}
|
||||
|
||||
// 图片上传
|
||||
const uploadImg = (file) => {
|
||||
// 压缩图片并上传
|
||||
new Compressor(file.file, {
|
||||
quality: 0.6,
|
||||
success(result) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', result, result.name);
|
||||
// 执行上传操作
|
||||
httpPost('/api/admin/upload', formData).then((res) => {
|
||||
role.value.icon = res.data.url
|
||||
ElMessage.success('上传成功')
|
||||
}).catch((e) => {
|
||||
ElMessage.error('上传失败:' + e.message)
|
||||
})
|
||||
},
|
||||
error(e) {
|
||||
ElMessage.error('上传失败:' + e.message)
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
@@ -257,6 +257,20 @@
|
||||
<el-tab-pane label="菜单配置" name="menu">
|
||||
<Menu/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="授权激活" name="license">
|
||||
<div class="container">
|
||||
<el-form :model="system" label-width="150px" label-position="right">
|
||||
<el-form-item label="许可授权码" prop="license">
|
||||
<el-input v-model="license"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="active">立即激活</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
@@ -275,8 +289,8 @@ const activeName = ref('basic')
|
||||
const system = ref({models: []})
|
||||
const loading = ref(true)
|
||||
const systemFormRef = ref(null)
|
||||
const chatFormRef = ref(null)
|
||||
const models = ref([])
|
||||
const openAIModels = ref([])
|
||||
const notice = ref("")
|
||||
|
||||
onMounted(() => {
|
||||
@@ -295,6 +309,7 @@ onMounted(() => {
|
||||
|
||||
httpGet('/api/admin/model/list').then(res => {
|
||||
models.value = res.data
|
||||
openAIModels.value = models.value.filter(v => v.platform === "OpenAI")
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取模型失败:" + e.message)
|
||||
@@ -320,19 +335,6 @@ const save = function (key) {
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (key === 'chat') {
|
||||
if (chat.value.context_deep % 2 !== 0) {
|
||||
return ElMessage.error("会话上下文深度必须为偶数!")
|
||||
}
|
||||
chatFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
httpPost('/api/admin/config/update', {key: key, config: chat.value}).then(() => {
|
||||
ElMessage.success("操作成功!")
|
||||
}).catch(e => {
|
||||
ElMessage.error("操作失败:" + e.message)
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (key === 'notice') {
|
||||
httpPost('/api/admin/config/update', {key: key, config: {content: notice.value, updated: true}}).then(() => {
|
||||
ElMessage.success("操作成功!")
|
||||
@@ -342,6 +344,19 @@ const save = function (key) {
|
||||
}
|
||||
}
|
||||
|
||||
// 激活授权
|
||||
const license = ref("")
|
||||
const active = () => {
|
||||
if (license.value === "") {
|
||||
return ElMessage.error("请输入授权码")
|
||||
}
|
||||
httpPost("/api/admin/active", {license: license.value}).then(res => {
|
||||
ElMessage.success("授权成功,机器编码为:" + res.data)
|
||||
}).catch(e => {
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const configKey = ref("")
|
||||
const beforeUpload = (key) => {
|
||||
configKey.value = key
|
||||
|
||||
@@ -19,7 +19,7 @@ import {ref} from "vue";
|
||||
import ImageMj from "@/views/mobile/ImageMj.vue";
|
||||
import ImageSd from "@/views/mobile/ImageSd.vue";
|
||||
|
||||
const activeName = ref("sd")
|
||||
const activeName = ref("mj")
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||