merge conflicts for v4.0.5

This commit is contained in:
RockYang
2024-05-21 11:30:40 +08:00
181 changed files with 5241 additions and 1437 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.3
VUE_APP_VERSION=v4.0.5

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.3
VUE_APP_VERSION=v4.0.5

1884
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,7 @@
"markmap-lib": "^0.16.1",
"markmap-view": "^0.16.0",
"md-editor-v3": "^2.2.1",
"mitt": "^3.0.1",
"pinia": "^2.1.4",
"qrcode": "^1.5.3",
"qs": "^6.11.1",

View File

@@ -32,7 +32,6 @@ window.ResizeObserver = class ResizeObserver extends _ResizeObserver {
<style lang="stylus">
html, body {
height: 100%;
margin: 0;
padding: 0;
}
@@ -67,4 +66,14 @@ html, body {
white-space: nowrap;
}
.van-toast--fail {
background #fef0f0
color #f56c6c
}
.van-toast--success {
background #D6FBCC
color #07C160
}
</style>

View File

@@ -1,30 +1,95 @@
.admin-home {
.header {
}
.dark {
color-scheme: dark;
--el-color-primary: #409eff;
--el-color-primary-light-3: #3375b9;
--el-color-primary-light-5: #2a598a;
--el-color-primary-light-7: #213d5b;
--el-color-primary-light-8: #1d3043;
--el-color-primary-light-9: #18222c;
--el-color-primary-dark-2: #66b1ff;
--el-color-success: #67c23a;
--el-color-success-light-3: #4e8e2f;
--el-color-success-light-5: #3e6b27;
--el-color-success-light-7: #2d481f;
--el-color-success-light-8: #25371c;
--el-color-success-light-9: #1c2518;
--el-color-success-dark-2: #85ce61;
--el-color-warning: #e6a23c;
--el-color-warning-light-3: #a77730;
--el-color-warning-light-5: #7d5b28;
--el-color-warning-light-7: #533f20;
--el-color-warning-light-8: #3e301c;
--el-color-warning-light-9: #292218;
--el-color-warning-dark-2: #ebb563;
--el-color-danger: #f56c6c;
--el-color-danger-light-3: #b25252;
--el-color-danger-light-5: #854040;
--el-color-danger-light-7: #582e2e;
--el-color-danger-light-8: #412626;
--el-color-danger-light-9: #2b1d1d;
--el-color-danger-dark-2: #f78989;
--el-color-error: #f56c6c;
--el-color-error-light-3: #b25252;
--el-color-error-light-5: #854040;
--el-color-error-light-7: #582e2e;
--el-color-error-light-8: #412626;
--el-color-error-light-9: #2b1d1d;
--el-color-error-dark-2: #f78989;
--el-color-info: #909399;
--el-color-info-light-3: #6b6d71;
--el-color-info-light-5: #525457;
--el-color-info-light-7: #393a3c;
--el-color-info-light-8: #2d2d2f;
--el-color-info-light-9: #202121;
--el-color-info-dark-2: #a6a9ad;
--el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.36), 0px 8px 20px rgba(0, 0, 0, 0.72);
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.72);
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.72);
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.72), 0px 12px 32px #000000, 0px 8px 16px -8px #000000;
--el-bg-color-page: #0a0a0a;
--el-bg-color: #141414;
--el-bg-color-overlay: #1d1e1f;
--el-text-color-primary: #E5EAF3;
--el-text-color-regular: #CFD3DC;
--el-text-color-secondary: #A3A6AD;
--el-text-color-placeholder: #8D9095;
--el-text-color-disabled: #6C6E72;
--el-border-color-darker: #636466;
--el-border-color-dark: #58585B;
--el-border-color: #4C4D4F;
--el-border-color-light: #414243;
--el-border-color-lighter: #363637;
--el-border-color-extra-light: #2B2B2C;
--el-fill-color-darker: #424243;
--el-fill-color-dark: #39393A;
--el-fill-color: #303030;
--el-fill-color-light: #262727;
--el-fill-color-lighter: #1D1D1D;
--el-fill-color-extra-light: #191919;
--el-fill-color-blank: transparent;
--el-mask-color: rgba(0, 0, 0, 0.8);
--el-mask-color-extra-light: rgba(0, 0, 0, 0.3)
--el-menu-bg-color-dark: #39393A
--el-menu-bg-color-darker: #424243
}
.login-wrap {
background: #324157;
}
.dark .el-button {
--el-button-disabled-text-color: rgba(255, 255, 255, 0.5)
}
.plugins-tips {
background: #eef1f6;
}
.dark .el-card {
--el-card-bg-color: var(--el-bg-color-overlay)
}
.plugins-tips a {
color: #20a0ff;
}
.tags-li.active {
border: 1px solid #409EFF;
background-color: #409EFF;
}
.message-title {
color: #20a0ff;
}
.collapse-btn:hover {
background: rgb(40, 52, 70);
}
.dark .el-empty {
--el-empty-fill-color-0: var(--el-color-black);
--el-empty-fill-color-1: #4b4b52;
--el-empty-fill-color-2: #36383d;
--el-empty-fill-color-3: #1e1e20;
--el-empty-fill-color-4: #262629;
--el-empty-fill-color-5: #202124;
--el-empty-fill-color-6: #212224;
--el-empty-fill-color-7: #1b1c1f;
--el-empty-fill-color-8: #1c1d1f;
--el-empty-fill-color-9: #18181a
}

View File

@@ -37,22 +37,28 @@ body {
transition: left .3s ease-in-out;
background: #f0f0f0;
::-webkit-scrollbar {
width: 8px; /* */
}
::-webkit-scrollbar-thumb {
background-color: #666666;
border-radius 8px
}
::-webkit-scrollbar-thumb:hover {
background-color: #999999;
}
.content {
width: auto;
height: 100vh;
overflow-y: scroll;
box-sizing: border-box;
.container {
padding: 15px 20px 30px 20px;
background: #fff;
border: 1px solid #ddd;
border-radius: 5px;
background: #ffffff;
margin-bottom 80px
.handle-box {
margin-bottom: 20px;
}
}
.crumbs {
@@ -60,7 +66,7 @@ body {
}
.el-table th {
background-color: #f5f7fa !important;
background-color: #f5f7fa;
}
.pagination {
@@ -142,6 +148,23 @@ body {
display: none !important;
}
}
.dark {
background: var(--el-bg-color);
.container {
background: var(--el-bg-color);
}
.crumbs {
margin: 10px 0;
}
.el-table th {
background-color: var(--el-fill-color-darker);
}
}
}
.content-collapse {

View File

@@ -1,5 +1,8 @@
.mobile-chat-list {
.content {
padding-top 46px
padding-bottom 60px
.van-list {
.van-cell__value {
.chat-list-item {
@@ -31,19 +34,4 @@
}
}
.van-popup {
.picker-option {
display flex
width 100%
padding 0 10px
overflow hidden
height 20px
text-overflow ellipsis
.van-image {
width 20px;
height 20px;
margin-right 5px
}
}
}
@import "model-select.styl"

View File

@@ -1,16 +1,29 @@
.mobile-chat {
.message-list-box {
//padding-top 50px
padding-bottom 10px
overflow-x auto
background #F5F5F5;
.van-nav-bar {
position static
.van-cell {
background none
font-family: 'Microsoft YaHei', '', Arial, sans-serif;
.setting {
font-size 18px
}
}
.chat-list-wrapper {
padding 10px 0 10px 0
background var(--van-background);
overflow hidden
.message-list-box {
overflow auto
.van-cell {
background none
font-family: 'Microsoft YaHei', '', Arial, sans-serif;
}
}
}
.chat-box-wrapper {
.van-sticky {
.van-cell-group--inset {
@@ -68,8 +81,10 @@
.van-theme-dark {
.mobile-chat {
.message-list-box {
.chat-list-wrapper {
background #232425;
}
}
}
}
@import "model-select.styl"

View File

@@ -8,7 +8,7 @@
.rate {
display: flex;
justify-content center
background-color #f5f5f5
background-color var(--van-background-3)
padding 5px 10px
margin 5px 0
border-radius 5px
@@ -24,14 +24,14 @@
.text {
text-align center
color #555555
color var(--van-text-color)
}
}
.model {
display: flex;
justify-content center
background-color #f5f5f5
background-color var(--van-background-3)
padding 6px
margin 5px 0
border-radius 5px
@@ -48,12 +48,12 @@
.text {
text-align center
color #555555
color var(--van-text-color)
}
}
.active {
background-color #e5e5e5
background-color var(--van-text-color-3)
}
}
}
@@ -74,6 +74,11 @@
}
color var(--van-text-color)
.pt-6 {
padding 15px 10px
}
.tip-text {
padding 10px
line-height 1.5

View File

@@ -80,8 +80,10 @@
}
color var(--van-text-color)
.pt-6 {
padding 10px
padding 15px 10px
}
.tip-text {

View File

@@ -0,0 +1,16 @@
.van-popup {
.picker-option {
display flex
width 100%
padding 0 10px
overflow hidden
height 20px
text-overflow ellipsis
.van-image {
width 20px;
height 20px;
margin-right 5px
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

View File

@@ -57,6 +57,7 @@ export default defineComponent({
if (!this.finalTokens) {
httpPost("/api/chat/tokens", {text: this.content, model: this.model}).then(res => {
this.finalTokens = res.data;
}).catch(() => {
})
}
}

View File

@@ -25,10 +25,8 @@
<script setup>
import {computed, onMounted, ref} from "vue"
import {httpGet, httpPost} from "@/utils/http";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {Plus} from "@element-plus/icons-vue";
import Compressor from "compressorjs";
import {dateFormat} from "@/utils/libs";
// eslint-disable-next-line no-undef

View File

@@ -106,7 +106,7 @@
autocomplete="off">
<template #prefix>
<el-icon>
<Iphone/>
<Message/>
</el-icon>
</template>
</el-input>

View File

@@ -9,17 +9,21 @@
>
<div class="form">
<el-form :model="form" label-width="120px" label-position="left">
<el-form :model="form" label-width="80px" label-position="left">
<el-form-item label="用户名">
<el-input v-model="form.username" placeholder="手机号/邮箱地址"/>
</el-form-item>
<el-form-item label="验证码">
<div class="code-box">
<el-input v-model="form.code" maxlength="6"/>
<send-msg size="" :receiver="form.username" style="margin-left: 10px; min-width: 100px"/>
</div>
<el-row :gutter="20">
<el-col :span="16">
<el-input v-model="form.code" maxlength="6"/>
<el-col :span="12">
</el-col>
<el-col :span="8">
<send-msg size="" :receiver="form.username"/>
<el-col :span="12" style="justify-content: right">
</el-col>
</el-row>
</el-form-item>
@@ -97,9 +101,13 @@ const close = function () {
<style lang="stylus">
.reset-pass {
.form {
padding 10px 40px
padding 10px 20px
}
.code-box {
display: flex;
justify-content: space-between;
width: 100%
}
.el-dialog__footer {
text-align center
}

View File

@@ -11,7 +11,7 @@
style="width: 360px;"
>
<slide-captcha
v-if="isIphone()"
v-if="isMobile()"
:bg-img="bgImg"
:bk-img="bkImg"
:result="result"
@@ -38,12 +38,13 @@
import {ref} from "vue";
import lodash from 'lodash'
import {validateEmail, validateMobile} from "@/utils/validate";
import {ElMessage} from "element-plus";
import {httpGet, httpPost} from "@/utils/http";
import CaptchaPlus from "@/components/CaptchaPlus.vue";
import SlideCaptcha from "@/components/SlideCaptcha.vue";
import {isIphone} from "@/utils/libs";
import {isMobile} from "@/utils/libs";
import {showMessageError, showMessageOK} from "@/utils/dialog";
// eslint-disable-next-line no-undef
const props = defineProps({
receiver: String,
size: String,
@@ -65,13 +66,13 @@ const handleRequestCaptCode = () => {
thumbBase64.value = data.thumb
captKey.value = data.key
}).catch(e => {
ElMessage.error('获取人机验证数据失败:' + e.message)
showMessageError('获取人机验证数据失败:' + e.message)
})
}
const handleConfirm = (dots) => {
if (lodash.size(dots) <= 0) {
return ElMessage.error('请进行人机验证再操作')
return showMessageError('请进行人机验证再操作')
}
let dotArr = []
@@ -87,19 +88,19 @@ const handleConfirm = (dots) => {
showCaptcha.value = false
sendMsg()
}).catch(() => {
ElMessage.error('人机验证失败')
showMessageError('人机验证失败')
handleRequestCaptCode()
})
}
const loadCaptcha = () => {
if (!validateMobile(props.receiver) && !validateEmail(props.receiver)) {
return ElMessage.error("请输入合法的手机号/邮箱地址")
return showMessageError("请输入合法的手机号/邮箱地址")
}
showCaptcha.value = true
// iphone 手机用滑动验证码
if (isIphone()) {
// 手机用滑动验证码
if (isMobile()) {
getSlideCaptcha()
} else {
handleRequestCaptCode()
@@ -113,7 +114,7 @@ const sendMsg = () => {
canSend.value = false
httpPost('/api/sms/code', {receiver: props.receiver, key: captKey.value, dots: dots.value}).then(() => {
ElMessage.success('验证码发送成功')
showMessageOK('验证码发送成功')
let time = 120
btnText.value = time
const handler = setInterval(() => {
@@ -128,7 +129,7 @@ const sendMsg = () => {
}, 1000)
}).catch(e => {
canSend.value = true
ElMessage.error('验证码发送失败:' + e.message)
showMessageError('验证码发送失败:' + e.message)
})
}
@@ -144,7 +145,7 @@ const getSlideCaptcha = () => {
bgImg.value = res.data.bgImg
captKey.value = res.data.key
}).catch(e => {
ElMessage.error('获取人机验证数据失败:' + e.message)
showMessageError('获取人机验证数据失败:' + e.message)
})
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="header admin-header">
<div :class="'admin-header '+theme">
<!-- 折叠按钮 -->
<div class="collapse-btn" @click="collapseChange">
<el-icon v-if="sidebar.collapse">
@@ -17,17 +17,15 @@
</div>
<div class="header-right">
<div class="header-user-con">
<!-- 消息中心 -->
<div class="btn-bell">
<el-tooltip
effect="dark"
:content="message ? `有${message}条未读消息` : `消息中心`"
placement="bottom"
>
<i class="iconfont icon-bell"></i>
</el-tooltip>
<span class="btn-bell-badge" v-if="message"></span>
</div>
<!-- 切换主题 -->
<el-switch
style="margin-right: 10px"
v-model="dark"
inline-prompt
:active-action-icon="Moon"
:inactive-action-icon="Sunny"
@change="changeTheme"
/>
<!-- 用户名下拉菜单 -->
<el-dropdown class="user-name" :hide-on-click="true" trigger="click">
<span class="el-dropdown-link">
@@ -38,20 +36,9 @@
</span>
<template #dropdown>
<el-dropdown-menu>
<a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
<el-dropdown-item>
<i class="iconfont icon-github"></i>
<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>
</el-dropdown-item>
<el-dropdown-item divided @click="logout">
<i class="iconfont icon-logout"></i>
<span>退出登录</span>
@@ -62,41 +49,39 @@
</div>
</div>
<el-dialog
v-model="showDialog"
:show-close="true"
class="donate-dialog"
width="400px"
title="请作者喝杯咖啡"
>
<el-alert type="info" :closable="false">
<p>如果你觉得这个项目对你有帮助并且情况允许的话可以请作者喝杯咖啡非常感谢你的支持</p>
</el-alert>
<p>
<el-image :src="donateImg"/>
</p>
</el-dialog>
</div>
</template>
<script setup>
import {onMounted, ref} from 'vue';
import {computed, onMounted, ref} from 'vue';
import {getMenuItems, useSidebarStore} from '@/store/sidebar';
import {useRouter} from 'vue-router';
import {ArrowDown, ArrowRight, Expand, Fold} from "@element-plus/icons-vue";
import {useRouter} from "vue-router";
import {ArrowDown, ArrowRight, Expand, Fold, Moon, Sunny} from "@element-plus/icons-vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
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)
const sidebar = useSidebarStore();
const router = useRouter();
const breadcrumb = ref([])
// eslint-disable-next-line no-undef
const props = defineProps({
theme: String,
});
const theme = computed(() => {
return props.theme
})
const dark = ref(props.theme === 'dark' ? true : false)
// eslint-disable-next-line no-undef
const emits = defineEmits(['changeTheme']);
const changeTheme = () => {
emits('changeTheme', dark.value)
}
router.afterEach((to, from) => {
initBreadCrumb(to.path)
@@ -166,7 +151,7 @@ const logout = function () {
}
</script>
<style scoped lang="stylus">
.header {
.admin-header {
position: relative;
box-sizing: border-box;
overflow hidden
@@ -174,7 +159,6 @@ const logout = function () {
font-size: 22px;
color: #303133;
background-color #ffffff
border-bottom 1px solid #eaecef
.collapse-btn {
display: flex;

View File

@@ -1,9 +1,9 @@
<template>
<div class="sidebar">
<div class="logo">
<div :class="'sidebar '+theme">
<a class="logo" href="/" target="_blank">
<el-image :src="logo"/>
<span class="text" v-show="!sidebar.collapse">{{ title }}</span>
</div>
</a>
<el-menu
class="sidebar-el-menu"
@@ -54,9 +54,9 @@
<script setup>
import {computed, ref} from 'vue';
import {setMenuItems, useSidebarStore} from '@/store/sidebar';
import {useRoute} from 'vue-router';
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {useRoute} from "vue-router";
const title = ref('Chat-Plus-Admin')
const logo = ref('/images/logo.png')
@@ -69,6 +69,15 @@ httpGet('/api/admin/config/get?key=system').then(res => {
ElMessage.error("加载系统配置失败: " + e.message)
})
// eslint-disable-next-line no-undef
const props = defineProps({
theme: String,
});
const theme = computed(() => {
return props.theme
})
const items = [
{
icon: 'home',
@@ -190,6 +199,7 @@ setMenuItems(items)
width 219px
background-color #324157
padding 6px 15px;
cursor pointer
.el-image {
width 36px;
@@ -234,4 +244,37 @@ setMenuItems(items)
width: 0;
}
.sidebar.dark {
border-right 1px solid var(--el-border-color-dark)
.logo {
background var(--el-bg-color)
border-right 1px solid var(--el-border-color)
.text {
color var(--el-text-color-regular)
}
}
ul {
background var(--el-bg-color)
.el-menu-item.is-active {
background-color var(--el-menu-bg-color-dark)
}
.el-menu-item:hover {
background-color var(--el-menu-bg-color-darker)
}
}
.sidebar-el-menu:not(.el-menu--collapse) {
width: 250px;
}
.el-menu {
border-color var(--el-border-color)
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="tags" v-if="tags.show">
<div :class="'tags '+theme" v-if="tags.show">
<ul>
<li
class="tags-li"
@@ -38,6 +38,16 @@ import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router';
import {ArrowDown, Close} from "@element-plus/icons-vue";
import {checkAdminSession} from "@/action/session";
import {ElMessageBox} from "element-plus";
import {computed} from "vue";
// eslint-disable-next-line no-undef
const props = defineProps({
theme: String,
});
const theme = computed(() => {
return props.theme
})
const router = useRouter();
checkAdminSession().catch(() => {
@@ -108,74 +118,75 @@ const handleTags = (command) => {
// });
</script>
<style>
<style scoped lang="stylus">
.tags {
position: relative;
height: 30px;
overflow: hidden;
background: #fff;
padding-right: 120px;
padding 5px 120px 5px 10px
-webkit-box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
ul {
box-sizing: border-box;
width: 100%;
height: 100%;
.tags-li {
display: flex;
align-items: center;
float: left;
margin: 3px 5px 2px 3px;
border-radius: 3px;
font-size: 12px;
overflow: hidden;
cursor: pointer;
height: 23px;
border: 1px solid var(--el-border-color);
background: var(--el-bg-color);
padding: 0 5px 0 12px;
color: var(--el-text-color);
-webkit-transition: all 0.3s ease-in;
-moz-transition: all 0.3s ease-in;
transition: all 0.3s ease-in;
&:hover {
background: var(--el-menu-bg-color-dark);
}
}
.tags-li-title {
float: left;
max-width: 80px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 5px;
color: #666;
}
.tags-li.active .tags-li-title {
color: var(--el-color-primary)
}
}
.tags-close-box {
position: absolute;
right: 0;
top: 7px;
box-sizing: border-box;
padding-top: 1px;
text-align: center;
width: 110px;
height: 30px;
background: var(--el-bg-color);
z-index: 10;
}
}
.tags ul {
box-sizing: border-box;
width: 100%;
height: 100%;
.tags.dark {
border-bottom 1px solid var(--el-border-color)
}
.tags-li {
display: flex;
align-items: center;
float: left;
margin: 3px 5px 2px 3px;
border-radius: 3px;
font-size: 12px;
overflow: hidden;
cursor: pointer;
height: 23px;
border: 1px solid #e9eaec;
background: #fff;
padding: 0 5px 0 12px;
color: #666;
-webkit-transition: all 0.3s ease-in;
-moz-transition: all 0.3s ease-in;
transition: all 0.3s ease-in;
}
.tags-li:not(.active):hover {
background: #f8f8f8;
}
.tags-li.active {
color: #fff;
}
.tags-li-title {
float: left;
max-width: 80px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 5px;
color: #666;
}
.tags-li.active .tags-li-title {
color: #fff;
}
.tags-close-box {
position: absolute;
right: 0;
top: 2px;
box-sizing: border-box;
padding-top: 1px;
text-align: center;
width: 110px;
height: 30px;
background: #fff;
//box-shadow: -3px 0 15px 3px rgba(0, 0, 0, 0.1); z-index: 10;
}
</style>

View File

@@ -16,6 +16,7 @@ import {onMounted, ref} from "vue";
import Clipboard from "clipboard";
import {showNotify} from "vant";
// eslint-disable-next-line no-unused-vars,no-undef
const props = defineProps({
content: {
type: String,

View File

@@ -1,3 +1,10 @@
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import {createApp} from 'vue'
import ElementPlus from "element-plus"
import "element-plus/dist/index.css"
@@ -5,6 +12,7 @@ import 'vant/lib/index.css';
import App from './App.vue'
import {createPinia} from "pinia";
import {
ActionSheet,
Badge,
Button,
Cell,
@@ -15,6 +23,7 @@ import {
CollapseItem,
ConfigProvider,
Dialog,
Divider,
DropdownItem,
DropdownMenu,
Empty,
@@ -29,6 +38,7 @@ import {
List,
Loading,
NavBar,
NoticeBar,
Notify,
Overlay,
Picker,
@@ -52,8 +62,8 @@ import {router} from "@/router";
import 'v3-waterfall/dist/style.css'
import V3waterfall from "v3-waterfall";
const app = createApp(App)
app.use(createPinia())
const app = createApp(App);
app.use(createPinia());
app.use(ConfigProvider);
app.use(Tabbar);
app.use(TabbarItem);
@@ -97,6 +107,9 @@ app.use(Lazyload);
app.use(ImagePreview);
app.use(Tab);
app.use(Tabs);
app.use(Divider);
app.use(NoticeBar);
app.use(ActionSheet);
app.use(router).use(ElementPlus).mount('#app')

View File

@@ -1,3 +1,10 @@
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import {createRouter, createWebHistory} from "vue-router";
const routes = [
@@ -201,8 +208,13 @@ const routes = [
path: '/mobile',
meta: {title: 'Geek-AI v4.0'},
component: () => import('@/views/mobile/Home.vue'),
redirect: '/mobile/chat',
redirect: '/mobile/index',
children: [
{
path: '/mobile/index',
name: 'mobile-index',
component: () => import('@/views/mobile/Index.vue'),
},
{
path: '/mobile/chat',
name: 'mobile-chat',
@@ -219,22 +231,22 @@ const routes = [
component: () => import('@/views/mobile/Profile.vue'),
},
{
path: '/mobile/img-wall',
path: '/mobile/imgWall',
name: 'mobile-img-wall',
component: () => import('@/views/mobile/ImgWall.vue'),
component: () => import('@/views/mobile/pages/ImgWall.vue'),
},
{
path: '/mobile/chat/session',
name: 'mobile-chat-session',
component: () => import('@/views/mobile/ChatSession.vue'),
},
{
path: '/mobile/chat/export',
name: 'mobile-chat-export',
component: () => import('@/views/mobile/ChatExport.vue'),
},
]
},
{
path: '/mobile/chat/session',
name: 'mobile-chat-session',
component: () => import('@/views/mobile/ChatSession.vue'),
},
{
path: '/mobile/chat/export',
name: 'mobile-chat-export',
component: () => import('@/views/mobile/ChatExport.vue'),
},
{
name: 'test',

View File

@@ -0,0 +1,6 @@
// 导入mitt包
import mitt from 'mitt'
// 创建EventBus实例对象
const bus = mitt()
// 共享出eventbus的实例对象
export default bus

View File

@@ -5,25 +5,11 @@ import Storage from "good-storage";
* storage handler
*/
const SessionIDKey = process.env.VUE_APP_KEY_PREFIX + 'SESSION_ID';
const UserTokenKey = process.env.VUE_APP_KEY_PREFIX + "Authorization";
const AdminTokenKey = process.env.VUE_APP_KEY_PREFIX + "Admin-Authorization"
export function getSessionId() {
let sessionId = Storage.get(SessionIDKey)
if (!sessionId) {
sessionId = randString(42)
setSessionId(sessionId)
}
return sessionId
}
export function removeSessionId() {
Storage.remove(SessionIDKey)
}
export function setSessionId(sessionId) {
Storage.set(SessionIDKey, sessionId)
return randString(42)
}
export function getUserToken() {

View File

@@ -1,6 +1,14 @@
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import Storage from "good-storage";
const MOBILE_THEME = process.env.VUE_APP_KEY_PREFIX + "MOBILE_THEME"
const ADMIN_THEME = process.env.VUE_APP_KEY_PREFIX + "ADMIN_THEME"
export function getMobileTheme() {
return Storage.get(MOBILE_THEME) ? Storage.get(MOBILE_THEME) : 'light'
@@ -8,4 +16,12 @@ export function getMobileTheme() {
export function setMobileTheme(theme) {
Storage.set(MOBILE_THEME, theme)
}
export function getAdminTheme() {
return Storage.get(ADMIN_THEME) ? Storage.get(ADMIN_THEME) : 'light'
}
export function setAdminTheme(theme) {
Storage.set(ADMIN_THEME, theme)
}

43
web/src/utils/dialog.js Normal file
View File

@@ -0,0 +1,43 @@
/**
* Util lib functions
*/
import {showConfirmDialog, showFailToast, showSuccessToast, showToast} from "vant";
import {isMobile} from "@/utils/libs";
import {ElMessage} from "element-plus";
export function showLoginDialog(router) {
showConfirmDialog({
title: '登录',
message:
'此操作需要登录才能进行,前往登录?',
}).then(() => {
router.push("/login")
}).catch(() => {
// on cancel
});
}
export function showMessageOK(message) {
if (isMobile()) {
showSuccessToast(message)
} else {
ElMessage.success(message)
}
}
export function showMessageInfo(message) {
if (isMobile()) {
showToast(message)
} else {
ElMessage.info(message)
}
}
export function showMessageError(message) {
if (isMobile()) {
showFailToast(message)
} else {
ElMessage.error(message)
}
}

View File

@@ -1,5 +1,12 @@
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import axios from 'axios'
import {getAdminToken, getSessionId, getUserToken} from "@/store/session";
import {getAdminToken, getSessionId, getUserToken, removeAdminToken, removeUserToken} from "@/store/session";
axios.defaults.timeout = 180000
axios.defaults.baseURL = process.env.VUE_APP_API_HOST
@@ -22,9 +29,14 @@ axios.interceptors.response.use(
let data = response.data;
if (data.code === 0) {
return response
} else {
return Promise.reject(response.data)
} else if (data.code === 400) {
if (response.request.responseURL.indexOf("/api/admin") !== -1) {
removeAdminToken()
} else {
removeUserToken()
}
}
return Promise.reject(response.data)
}, error => {
return Promise.reject(error)
})

View File

@@ -1,6 +1,14 @@
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
/**
* Util lib functions
*/
import {showConfirmDialog} from "vant";
// generate a random string
export function randString(length) {
@@ -224,3 +232,15 @@ export function escapeHTML(html) {
export function isIphone() {
return /iPhone/i.test(navigator.userAgent) && !/iPad/i.test(navigator.userAgent);
}
export function showLoginDialog(router) {
showConfirmDialog({
title: '登录',
message:
'此操作需要登录才能进行,前往登录?',
}).then(() => {
router.push("/login")
}).catch(() => {
// on cancel
});
}

View File

@@ -13,7 +13,7 @@
<span class="name">{{ scope.item.name }}</span>
<div class="opt">
<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="success" @click="useRole(scope.item)">使用</el-button>
<el-button size="small" type="danger" @click="updateRole(scope.item,'remove')">移除</el-button>
</div>
<el-button v-else size="small"
@@ -110,8 +110,8 @@ const hasRole = (roleKey) => {
}
const router = useRouter()
const useRole = (roleId) => {
router.push({name: "chat", params: {role_id: roleId}})
const useRole = (role) => {
router.push(`/chat?role_id=${role.id}`)
}
</script>

View File

@@ -286,7 +286,7 @@ const notice = ref("")
const noticeKey = ref("SYSTEM_NOTICE")
if (isMobile()) {
router.replace("/mobile")
router.replace("/mobile/chat")
}
// 获取系统配置
@@ -330,7 +330,10 @@ onMounted(() => {
});
onUnmounted(() => {
socket.value = null
if (socket.value !== null) {
socket.value.close()
socket.value = null
}
})
// 初始化数据
@@ -355,9 +358,8 @@ const initData = () => {
// 加载角色列表
httpGet(`/api/role/list`).then((res) => {
roles.value = res.data;
console.log()
if (router.currentRoute.value.params.role_id) {
roleId.value = parseInt(router.currentRoute.value.params["role_id"])
if (router.currentRoute.value.query.role_id) {
roleId.value = parseInt(router.currentRoute.value.query.role_id)
} else {
roleId.value = roles.value[0]['id']
}
@@ -652,55 +654,63 @@ const connect = function (chat_id, role_id) {
});
_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));
if (data.type === 'start') {
console.log(data)
chatData.value.push({
type: "reply",
id: randString(32),
icon: _role['icon'],
content: ""
});
} else if (data.type === 'end') { // 消息接收完毕
// 追加当前会话到会话列表
if (isNewChat && newChatItem.value !== null) {
newChatItem.value['title'] = previousText.value;
newChatItem.value['chat_id'] = chat_id;
chatList.value.unshift(newChatItem.value);
activeChat.value = newChatItem.value;
newChatItem.value = null; // 只追加一次
}
try {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
const data = JSON.parse(String(reader.result));
if (data.type === 'start') {
chatData.value.push({
type: "reply",
id: randString(32),
icon: _role['icon'],
content: ""
});
} else if (data.type === 'end') { // 消息接收完毕
// 追加当前会话到会话列表
if (isNewChat && newChatItem.value !== null) {
newChatItem.value['title'] = previousText.value;
newChatItem.value['chat_id'] = chat_id;
chatList.value.unshift(newChatItem.value);
activeChat.value = newChatItem.value;
newChatItem.value = null; // 只追加一次
}
enableInput()
lineBuffer.value = ''; // 清空缓冲
enableInput()
lineBuffer.value = ''; // 清空缓冲
// 获取 token
const reply = chatData.value[chatData.value.length - 1]
httpPost("/api/chat/tokens", {text: "", model: getModelValue(modelID.value), chat_id: chat_id}).then(res => {
reply['created_at'] = new Date().getTime();
reply['tokens'] = res.data;
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
// 获取 token
const reply = chatData.value[chatData.value.length - 1]
httpPost("/api/chat/tokens", {
text: "",
model: getModelValue(modelID.value),
chat_id: chat_id
}).then(res => {
reply['created_at'] = new Date().getTime();
reply['tokens'] = res.data;
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
})
}).catch(() => {
})
})
} else {
lineBuffer.value += data.content;
const reply = chatData.value[chatData.value.length - 1]
reply['orgContent'] = lineBuffer.value;
reply['content'] = md.render(processContent(lineBuffer.value));
}
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
localStorage.setItem("chat_id", chat_id)
})
};
} else {
lineBuffer.value += data.content;
const reply = chatData.value[chatData.value.length - 1]
reply['orgContent'] = lineBuffer.value;
reply['content'] = md.render(processContent(lineBuffer.value));
}
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
localStorage.setItem("chat_id", chat_id)
})
};
}
} catch (e) {
console.error(e)
}
});
@@ -901,14 +911,6 @@ const exportChat = () => {
window.open(url, '_blank');
}
const getChatById = (chatId) => {
for (let index in chatList.value) {
if (chatList.value[index].chat_id === chatId) {
return chatList.value[index]
}
}
return null
}
const getModelValue = (model_id) => {
for (let i = 0; i < models.value.length; i++) {

View File

@@ -105,7 +105,7 @@
</div>
<h2>创作记录</h2>
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.5)">
<div class="finish-job-list">
<div v-if="finishedJobs.length > 0">
<ItemList :items="finishedJobs" :width="240" :gap="16">
<template #default="scope">
@@ -272,7 +272,6 @@ const initData = () => {
fetchFinishJobs(1)
connect()
}).catch(() => {
loading.value = false
});
}
@@ -341,6 +340,9 @@ const connect = () => {
}
const fetchRunningJobs = () => {
if (!isLogin.value) {
return
}
// 获取运行中的任务
httpGet(`/api/dall/jobs?status=0`).then(res => {
const jobs = res.data
@@ -367,10 +369,11 @@ const fetchRunningJobs = () => {
const page = ref(1)
const pageSize = ref(15)
const isOver = ref(false)
const loading = ref(false)
// 获取已完成的任务
const fetchFinishJobs = (page) => {
loading.value = true
if (!isLogin.value) {
return
}
httpGet(`/api/dall/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
if (res.data.length < pageSize.value) {
isOver.value = true
@@ -380,9 +383,7 @@ const fetchFinishJobs = (page) => {
} else {
finishedJobs.value = finishedJobs.value.concat(res.data)
}
loading.value = false
}).catch(e => {
loading.value = false
ElMessage.error("获取任务失败:" + e.message)
})
}

View File

@@ -493,7 +493,7 @@
</div>
<h2>创作记录</h2>
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.5)">
<div class="finish-job-list">
<div v-if="finishedJobs.length > 0">
<ItemList :items="finishedJobs" :width="240" :gap="16">
<template #default="scope">
@@ -593,7 +593,7 @@
</template>
<script setup>
import {nextTick, onMounted, onUnmounted, ref} from "vue"
import {onMounted, onUnmounted, ref} from "vue"
import {ChromeFilled, Delete, DocumentCopy, InfoFilled, Picture, Plus, UploadFilled} from "@element-plus/icons-vue";
import Compressor from "compressorjs";
import {httpGet, httpPost} from "@/utils/http";
@@ -760,7 +760,11 @@ onMounted(() => {
})
onUnmounted(() => {
socket.value = null
clipboard.value.destroy()
if (socket.value !== null) {
socket.value.close()
socket.value = null
}
})
// 初始化数据
@@ -778,10 +782,6 @@ const initData = () => {
});
}
onUnmounted(() => {
clipboard.value.destroy()
})
const mjPower = ref(1)
const mjActionPower = ref(1)
httpGet("/api/config/get?key=system").then(res => {
@@ -793,6 +793,10 @@ httpGet("/api/config/get?key=system").then(res => {
// 获取运行中的任务
const fetchRunningJobs = () => {
if (!isLogin.value) {
return
}
httpGet(`/api/mj/jobs?status=0`).then(res => {
const jobs = res.data
const _jobs = []
@@ -832,9 +836,11 @@ const handleScrollEnd = () => {
const page = ref(1)
const pageSize = ref(15)
const isOver = ref(false)
const loading = ref(false)
const fetchFinishJobs = (page) => {
loading.value = true
if (!isLogin.value) {
return
}
// 获取已完成的任务
httpGet(`/api/mj/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
const jobs = res.data
@@ -861,9 +867,7 @@ const fetchFinishJobs = (page) => {
} else {
finishedJobs.value = finishedJobs.value.concat(jobs)
}
nextTick(() => loading.value = false)
}).catch(e => {
loading.value = false
ElMessage.error("获取任务失败:" + e.message)
})
}

View File

@@ -315,7 +315,7 @@
<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 class="finish-job-list">
<div v-if="finishedJobs.length > 0">
<ItemList :items="finishedJobs" :width="240" :gap="16">
<template #default="scope">
@@ -616,7 +616,10 @@ onMounted(() => {
onUnmounted(() => {
clipboard.value.destroy()
socket.value = null
if (socket.value !== null) {
socket.value.close()
socket.value = null
}
})
@@ -629,11 +632,14 @@ const initData = () => {
fetchFinishJobs()
connect()
}).catch(() => {
loading.value = false
});
}
const fetchRunningJobs = () => {
if (!isLogin.value) {
return
}
// 获取运行中的任务
httpGet(`/api/sd/jobs?status=0`).then(res => {
const jobs = res.data
@@ -668,10 +674,11 @@ const handleScrollEnd = () => {
const page = ref(1)
const pageSize = ref(15)
const isOver = ref(false)
const loading = ref(false)
// 获取已完成的任务
const fetchFinishJobs = (page) => {
loading.value = true
if (!isLogin.value) {
return
}
httpGet(`/api/sd/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
if (res.data.length < pageSize.value) {
isOver.value = true
@@ -681,9 +688,7 @@ const fetchFinishJobs = (page) => {
} else {
finishedJobs.value = finishedJobs.value.concat(res.data)
}
loading.value = false
}).catch(e => {
loading.value = false
ElMessage.error("获取任务失败:" + e.message)
})
}

View File

@@ -5,9 +5,9 @@
<h2>AI 绘画作品墙</h2>
<div class="settings">
<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 value="mj" size="large">MidJourney</el-radio>
<el-radio value="sd" size="large">Stable Diffusion</el-radio>
<el-radio value="dall" size="large">DALL-E</el-radio>
</el-radio-group>
</div>
</div>
@@ -72,7 +72,7 @@
</template>
</v3-waterfall>
<v3-waterfall v-if="imgType === 'dall'"
<v3-waterfall v-else-if="imgType === 'dall'"
id="waterfall"
:list="data['dall']"
srcKey="img_thumb"
@@ -338,7 +338,6 @@ const getNext = () => {
loading.value = true
page.value = page.value + 1
let url = ""
console.log(imgType.value)
switch (imgType.value) {
case "mj":
url = "/api/mj/imgWall"

View File

@@ -1,5 +1,6 @@
<template>
<div class="index-page" :style="{height: winHeight+'px'}">
<div class="bg"></div>
<div class="menu-box">
<el-menu
mode="horizontal"
@@ -23,6 +24,8 @@
<span>项目源码</span>
</el-button>
</a>
<el-button @click="router.push('/login')" round>登录</el-button>
<el-button @click="router.push('/register')" round>注册</el-button>
</div>
</el-menu>
</div>
@@ -31,22 +34,22 @@
<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>
<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>
<span>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>
<span>SD 绘画</span>
</el-button>
<el-button @click="router.push('/xmind')" color="#FFFD55" style="color:#424282" :dark="false">
<i class="iconfont icon-xmind"></i>
<span>思维导图</span>
</el-button>
<div id="animation-container"></div>
<!-- <div id="animation-container"></div>-->
</div>
<div class="footer">
@@ -57,19 +60,24 @@
<script setup>
import * as THREE from 'three';
// 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";
import {isMobile} from "@/utils/libs";
const router = useRouter()
if (isMobile()) {
router.push("/mobile")
}
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 size = Math.max(window.innerWidth * 0.5, window.innerHeight * 0.8)
const winHeight = window.innerHeight - 150
onMounted(() => {
@@ -83,59 +91,59 @@ onMounted(() => {
})
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();
}
);
// // 创建场景
// // 创建场景
// 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>
@@ -143,7 +151,6 @@ const init = () => {
@import '@/assets/iconfont/iconfont.css'
.index-page {
margin: 0
background-color #007bff /* 科技蓝色背景 */
overflow hidden
color #ffffff
display flex
@@ -151,6 +158,18 @@ const init = () => {
align-items baseline
padding-top 150px
.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;
}
.menu-box {
position absolute
top 0

View File

@@ -4,7 +4,7 @@
<div class="main">
<div class="contain">
<div class="logo">
<el-image :src="logo" fit="cover"/>
<el-image :src="logo" fit="cover" @click="router.push('/')"/>
</div>
<div class="header">{{ title }}</div>
<div class="content">
@@ -34,9 +34,10 @@
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
</el-row>
<el-row class="text-line" gutter="20">
<el-button type="primary" @click="router.push('/register')" size="small" plain>注册新账号</el-button>
<el-button type="success" @click="showResetPass = true" size="small" plain>重置密码</el-button>
<el-row class="opt" :gutter="20">
<el-col :span="8"><el-link type="primary" @click="router.push('/register')">注册</el-link></el-col>
<el-col :span="8"><el-link @click="showResetPass = true">重置密码</el-link></el-col>
<el-col :span="8"><el-link @click="router.push('/')">首页</el-link></el-col>
</el-row>
</div>
</div>
@@ -55,17 +56,16 @@
import {ref} from "vue";
import {Lock, UserFilled} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {useRouter} from "vue-router";
import FooterBar from "@/components/FooterBar.vue";
import {isMobile} from "@/utils/libs";
import {checkSession} from "@/action/session";
import {setUserToken} from "@/store/session";
import {prevRoute} from "@/router";
import ResetPass from "@/components/ResetPass.vue";
import {showMessageError} from "@/utils/dialog";
const router = useRouter();
const title = ref('ChatPlus 用户登录');
const title = ref('Geek-AI');
const username = ref(process.env.VUE_APP_USER);
const password = ref(process.env.VUE_APP_PASS);
const showResetPass = ref(false)
@@ -74,8 +74,9 @@ const logo = ref("/images/logo.png")
// 获取系统配置
httpGet("/api/config/get?key=system").then(res => {
logo.value = res.data.logo
title.value = res.data.title
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
showMessageError("获取系统配置失败:" + e.message)
})
@@ -96,30 +97,25 @@ const handleKeyup = (e) => {
const login = function () {
if (username.value.trim() === '') {
return ElMessage.error("请输入用户民")
return showMessageError("请输入用户民")
}
if (password.value.trim() === '') {
return ElMessage.error('请输入密码');
return showMessageError('请输入密码');
}
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
setUserToken(res.data)
if (prevRoute.path === '' || prevRoute.path === '/register') {
if (isMobile()) {
router.push('/mobile')
} else {
router.push('/chat')
}
if (isMobile()) {
router.push('/mobile')
} else {
router.push(prevRoute.path)
router.push('/chat')
}
}).catch((e) => {
ElMessage.error('登录失败,' + e.message)
showMessageError('登录失败,' + e.message)
})
}
</script>
<style lang="stylus" scoped>
@@ -154,6 +150,7 @@ const login = function () {
.el-image {
width 120px;
cursor pointer
}
}
@@ -164,6 +161,7 @@ const login = function () {
color $white_v1
letter-space 2px
text-align center
padding-top 10px
}
.content {
@@ -198,6 +196,13 @@ const login = function () {
padding-top 10px;
font-size 14px;
}
.opt {
padding 15px
.el-col {
text-align center
}
}
}
}

View File

@@ -99,7 +99,6 @@
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";
@@ -129,9 +128,6 @@ 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)
@@ -142,8 +138,12 @@ const loading = ref(false)
onMounted(() => {
initData()
markMap.value = Markmap.create(svgRef.value)
update()
try {
markMap.value = Markmap.create(svgRef.value)
update()
} catch (e) {
console.error(e)
}
});
const initData = () => {
@@ -168,9 +168,13 @@ const initData = () => {
const update = () => {
const {root} = transformer.transform(processContent(text.value))
markMap.value.setData(root)
markMap.value.fit()
try {
const {root} = transformer.transform(processContent(text.value))
markMap.value.setData(root)
markMap.value.fit()
} catch (e) {
console.error(e)
}
}
const processContent = (text) => {
@@ -230,7 +234,6 @@ const connect = (userId) => {
const _socket = new WebSocket(host + `/api/markMap/client?user_id=${userId}&model_id=${modelID.value}`);
_socket.addEventListener('open', () => {
socket.value = _socket;
// 发送心跳消息
sendHeartbeat()
});
@@ -265,9 +268,10 @@ const connect = (userId) => {
_socket.addEventListener('close', () => {
loading.value = false
if (socket.value !== null) {
checkSession().then(() => {
connect(userId)
}
}).catch(() => {
})
});
}

View File

@@ -5,29 +5,100 @@
<div class="page-inner">
<div class="contain" v-if="enableRegister">
<div class="logo">
<el-image :src="logo" fit="cover"/>
<el-image :src="logo" fit="cover" @click="router.push('/')"/>
</div>
<div class="header">{{ title }}</div>
<div class="content">
<el-form :model="formData" label-width="120px" ref="formRef">
<div class="block">
<el-input :placeholder="placeholder"
size="large"
v-model="formData.username"
autocomplete="off">
<template #prefix>
<el-icon>
<Iphone/>
</el-icon>
</template>
</el-input>
</div>
<el-form :model="data" class="form" v-if="enableRegister">
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="手机注册" name="mobile" v-if="enableMobile">
<div class="block">
<el-input placeholder="手机号码"
size="large"
v-model="data.username"
maxlength="11"
autocomplete="off">
<template #prefix>
<el-icon>
<Iphone/>
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-row :gutter="10">
<el-col :span="12">
<el-input placeholder="验证码"
size="large" maxlength="30"
v-model="data.code"
autocomplete="off">
<template #prefix>
<el-icon>
<Checked/>
</el-icon>
</template>
</el-input>
</el-col>
<el-col :span="12">
<send-msg size="large" :receiver="data.username"/>
</el-col>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane label="邮箱注册" name="email" v-if="enableEmail">
<div class="block">
<el-input placeholder="邮箱地址"
size="large"
v-model="data.username"
autocomplete="off">
<template #prefix>
<el-icon>
<Message/>
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-row :gutter="10">
<el-col :span="12">
<el-input placeholder="验证码"
size="large" maxlength="30"
v-model="data.code"
autocomplete="off">
<template #prefix>
<el-icon>
<Checked/>
</el-icon>
</template>
</el-input>
</el-col>
<el-col :span="12">
<send-msg size="large" :receiver="data.username"/>
</el-col>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane label="用户名注册" name="username" v-if="enableUser">
<div class="block">
<el-input placeholder="用户名"
size="large"
v-model="data.username"
autocomplete="off">
<template #prefix>
<el-icon>
<Iphone/>
</el-icon>
</template>
</el-input>
</div>
</el-tab-pane>
</el-tabs>
<div class="block">
<el-input placeholder="请输入密码(8-16位)"
maxlength="16" size="large"
v-model="formData.password" show-password
v-model="data.password" show-password
autocomplete="off">
<template #prefix>
<el-icon>
@@ -39,7 +110,7 @@
<div class="block">
<el-input placeholder="重复密码(8-16位)"
size="large" maxlength="16" v-model="formData.repass" show-password
size="large" maxlength="16" v-model="data.repass" show-password
autocomplete="off">
<template #prefix>
<el-icon>
@@ -49,30 +120,10 @@
</el-input>
</div>
<div class="block" v-if="enableMobile || enableEmail">
<el-row :gutter="10">
<el-col :span="12">
<el-input placeholder="验证码"
size="large" maxlength="30"
v-model="formData.code"
autocomplete="off">
<template #prefix>
<el-icon>
<Checked/>
</el-icon>
</template>
</el-input>
</el-col>
<el-col :span="12">
<send-msg size="large" :receiver="formData.username"/>
</el-col>
</el-row>
</div>
<div class="block">
<el-input placeholder="邀请码"
<el-input placeholder="邀请码(可选)"
size="large"
v-model="formData.invite_code"
v-model="data.invite_code"
autocomplete="off">
<template #prefix>
<el-icon>
@@ -82,8 +133,10 @@
</el-input>
</div>
<el-row class="btn-row">
<el-button class="login-btn" size="large" type="primary" @click="register">注册</el-button>
<el-row class="btn-row" :gutter="20">
<el-col :span="24">
<el-button class="login-btn" type="primary" size="large" @click="submitRegister">注册</el-button>
</el-col>
</el-row>
<el-row class="text-line">
@@ -91,6 +144,8 @@
<el-link type="primary" @click="router.push('/login')">登录</el-link>
</el-row>
</el-form>
</div>
</div>
@@ -124,78 +179,86 @@ import FooterBar from "@/components/FooterBar.vue";
import SendMsg from "@/components/SendMsg.vue";
import {arrayContains} from "@/utils/libs";
import {setUserToken} from "@/store/session";
import {validateEmail, validateMobile} from "@/utils/validate";
import {showMessageError, showMessageOK} from "@/utils/dialog";
const router = useRouter();
const title = ref('ChatPlus 用户注册');
const formData = ref({
const title = ref('Geek-AI 用户注册');
const logo = ref("/images/logo")
const data = ref({
username: '',
password: '',
code: '',
repass: '',
invite_code: router.currentRoute.value.query['invite_code'],
})
const formRef = ref(null)
const enableMobile = ref(false)
const enableEmail = ref(false)
const enableRegister = ref(true)
const enableUser = ref(false)
const enableRegister = ref(false)
const activeName = ref("mobile")
const wxImg = ref("/images/wx.png")
const ways = []
const placeholder = ref("用户名:")
const logo = ref("/images/logo.png")
httpGet("/api/config/get?key=system").then(res => {
if (res.data) {
title.value = res.data.title
logo.value = res.data.logo
const registerWays = res.data['register_ways']
if (arrayContains(registerWays, "mobile")) {
enableMobile.value = true
ways.push("手机号")
}
if (arrayContains(registerWays, "email")) {
enableEmail.value = true
ways.push("邮箱地址")
}
placeholder.value += ways.join("/")
if (arrayContains(registerWays, "username")) {
enableUser.value = true
}
// 是否启用注册
enableRegister.value = res.data['enabled_register']
// 覆盖微信二维码
// 使用后台上传的客服微信二维码
if (res.data['wechat_card_url'] !== '') {
wxImg.value = res.data['wechat_card_url']
}
logo.value = res.data.logo
}
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
httpGet("/api/invite/hits", {code: formData.value.invite_code}).then(() => {
}).catch(() => {
})
const register = function () {
if (formData.value.username === '') {
return ElMessage.error('请输入用户名');
// 注册操作
const submitRegister = () => {
if (data.value.username === '') {
return showMessageError('请输入用户名');
}
if (formData.value.password.length < 8) {
return ElMessage.error('密码的长度为8-16个字符');
}
if (formData.value.repass !== formData.value.password) {
return ElMessage.error('两次输入密码不一致');
if (activeName.value === 'mobile' && !validateMobile(data.value.username)) {
return showMessageError('请输入合法的手机号');
}
if ((enableEmail.value || enableMobile.value) && formData.value.code === '') {
return ElMessage.error('请输入验证码');
if (activeName.value === 'email' && !validateEmail(data.value.username)) {
return showMessageError('请输入合法的邮箱地址');
}
httpPost('/api/user/register', formData.value).then((res) => {
if (data.value.password.length < 8) {
return showMessageError('密码的长度为8-16个字符');
}
if (data.value.repass !== data.value.password) {
return showMessageError('两次输入密码不一致');
}
if ((activeName.value === 'mobile' || activeName.value === 'email') && data.value.code === '') {
return showMessageError('请输入验证码');
}
data.value.reg_way = activeName.value
httpPost('/api/user/register', data.value).then((res) => {
setUserToken(res.data)
ElMessage.success({
showMessageOK({
"message": "注册成功,即将跳转到对话主界面...",
onClose: () => router.push("/chat"),
duration: 1000
})
}).catch((e) => {
ElMessage.error('注册失败,' + e.message)
showMessageError('注册失败,' + e.message)
})
}
@@ -222,7 +285,7 @@ const register = function () {
.page-inner {
max-width 450px
min-width 360px
width 100%
height 100vh
display flex
justify-content center
@@ -234,13 +297,14 @@ const register = function () {
color #ffffff
border-radius 10px;
z-index 10
background-color rgba(255, 255, 255, 0.3)
background-color rgba(255, 255, 255, 0.2)
.logo {
text-align center
.el-image {
width 120px;
cursor pointer
}
}
@@ -251,6 +315,7 @@ const register = function () {
color $white_v1
letter-space 2px
text-align center
padding-top 10px
}
.content {
@@ -326,5 +391,9 @@ const register = function () {
color #c1c1c1
}
}
.el-tabs__item {
color #ffffff
}
}
</style>

View File

@@ -284,15 +284,8 @@ const changePlatform = () => {
<style lang="stylus" scoped>
.list {
.opt-box {
padding-bottom: 10px;
display: flex;
justify-content flex-end
.el-icon {
margin-right: 5px;
}
.handle-box {
margin-bottom 20px
}
.copy-key {
@@ -303,6 +296,12 @@ const changePlatform = () => {
.el-select {
width: 100%
}
.pagination {
padding 20px 0
display flex
justify-content righ
}
}
.el-form {

View File

@@ -178,7 +178,7 @@
import {onMounted, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {dateFormat, processContent, removeArrayItem} from "@/utils/libs";
import {dateFormat, processContent} from "@/utils/libs";
import {Search} from "@element-plus/icons-vue";
import 'highlight.js/styles/a11y-dark.css'
import hl from "highlight.js";
@@ -343,8 +343,8 @@ const showMessages = (row) => {
<style lang="stylus" scoped>
.chat-list {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
@@ -365,6 +365,12 @@ const showMessages = (row) => {
width: 100%
}
.pagination {
padding 20px 0
display flex
justify-content right
}
.chat-box {
overflow-y: auto;
overflow-x hidden

View File

@@ -350,13 +350,14 @@ const remove = function (row) {
@import "@/assets/css/admin/form.styl";
.model-list {
.opt-box {
padding-bottom: 10px;
display: flex;
justify-content flex-end
.handle-box {
margin-bottom 20px
}
.el-icon {
margin-right: 5px;
.cell {
.copy-model {
margin-left 6px
cursor pointer
}
}
@@ -371,5 +372,11 @@ const remove = function (row) {
width: 100%
}
.pagination {
padding 20px 0
display flex
justify-content right
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="container role-list" v-loading="loading">
<div class="container function" v-loading="loading">
<div class="handle-box">
<el-button type="primary" :icon="Plus" @click="addRow">新增</el-button>
</div>
@@ -296,15 +296,9 @@ const generateToken = () => {
</script>
<style lang="stylus" scoped>
.role-list {
.opt-box {
padding-bottom: 10px;
display flex;
justify-content flex-end
.el-icon {
margin-right 5px;
}
.function {
.handle-box {
margin-bottom 20px
}
.param-line {
@@ -334,5 +328,11 @@ const generateToken = () => {
text-align center
}
}
.pagination {
padding 20px 0
display flex
justify-content right
}
}
</style>

View File

@@ -1,10 +1,10 @@
<template>
<div class="admin-home" v-if="isLogin">
<admin-sidebar/>
<admin-sidebar v-model:theme="theme"/>
<div class="content-box" :class="{ 'content-collapse': sidebar.collapse }">
<admin-header/>
<admin-tags/>
<div class="content">
<admin-header v-model:theme="theme" @changeTheme="changeTheme"/>
<admin-tags v-model:theme="theme"/>
<div :class="'content '+theme" :style="{height:contentHeight+'px'}">
<router-view v-slot="{ Component }">
<transition name="move" mode="out-in">
<keep-alive :include="tags.nameList">
@@ -25,10 +25,13 @@ import AdminTags from "@/components/admin/AdminTags.vue";
import {useRouter} from "vue-router";
import {checkAdminSession} from "@/action/session";
import {ref} from "vue";
import {getAdminTheme, setAdminTheme} from "@/store/system";
const sidebar = useSidebarStore();
const tags = useTagsStore();
const isLogin = ref(false)
const contentHeight = window.innerHeight - 80
const theme = ref(getAdminTheme())
// 获取会话信息
const router = useRouter();
@@ -37,10 +40,20 @@ checkAdminSession().then(() => {
}).catch(() => {
router.replace('/admin/login')
})
const changeTheme = (value) => {
if (value) {
theme.value = 'dark'
} else {
theme.value = 'light'
}
setAdminTheme(theme.value)
}
</script>
<style scoped lang="stylus">
@import '@/assets/css/main.css';
@import '@/assets/css/color-dark.css';
@import '@/assets/css/color-dark.styl';
@import '@/assets/css/main.styl';
@import '@/assets/iconfont/iconfont.css';
</style>

View File

@@ -63,19 +63,19 @@ const fetchList = function (_page, _pageSize) {
<style lang="stylus" scoped>
.list {
.opt-box {
padding-bottom: 10px;
display flex;
justify-content flex-start
.el-icon {
margin-right: 5px;
}
.handle-box {
margin-bottom 20px
}
.el-select {
width: 100%
}
.pagination {
padding 20px 0
display flex
justify-content right
}
}
</style>

View File

@@ -74,7 +74,6 @@ import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox} from "element-plus";
import {dateFormat, removeArrayItem} from "@/utils/libs";
import {Plus} from "@element-plus/icons-vue";
import {Sortable} from "sortablejs";
// 变量定义
const items = ref([])
@@ -170,19 +169,19 @@ const remove = function (row) {
<style lang="stylus" scoped>
.list {
.opt-box {
padding-bottom: 10px;
display flex;
justify-content flex-end
.el-icon {
margin-right: 5px;
}
.handle-box {
margin-bottom 20px
}
.el-select {
width: 100%
}
.pagination {
padding 20px 0
display flex
justify-content right
}
}
</style>

View File

@@ -129,6 +129,7 @@ const remove = function (row) {
.order {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
@@ -149,5 +150,11 @@ const remove = function (row) {
width: 100%
}
.pagination {
padding 20px 0
display flex
justify-content right
}
}
</style>

View File

@@ -68,7 +68,7 @@
<script setup>
import {onMounted, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {dateFormat} from "@/utils/libs";
import {Search} from "@element-plus/icons-vue";
@@ -135,6 +135,7 @@ const fetchData = () => {
<style lang="stylus" scoped>
.power-log {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
@@ -155,5 +156,12 @@ const fetchData = () => {
width: 100%
}
.pagination {
padding 20px 0
display flex
width 100%
justify-content right
}
}
</style>

View File

@@ -207,6 +207,14 @@ const remove = function (row) {
<style lang="stylus" scoped>
.product {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
}
}
.opt-box {
padding-bottom: 10px;
@@ -222,5 +230,11 @@ const remove = function (row) {
width: 100%
}
.pagination {
padding 20px 0
display flex
justify-content right
}
}
</style>

View File

@@ -82,6 +82,14 @@ const remove = function (row) {
<style lang="stylus" scoped>
.list {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
}
}
.opt-box {
padding-bottom: 10px;
@@ -97,5 +105,11 @@ const remove = function (row) {
width: 100%
}
.pagination {
padding 20px 0
display flex
justify-content right
}
}
</style>

View File

@@ -322,6 +322,15 @@ const uploadImg = (file) => {
<style lang="stylus" scoped>
.role-list {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
}
}
.opt-box {
padding-bottom: 10px;
display flex;
@@ -360,5 +369,11 @@ const uploadImg = (file) => {
text-align center
}
}
.pagination {
padding 20px 0
display flex
justify-content right
}
}
</style>

View File

@@ -258,6 +258,52 @@
<Menu/>
</el-tab-pane>
<el-tab-pane label="授权激活" name="license">
<div class="container">
<el-descriptions
v-if="license.is_active"
class="margin-top"
title="授权信息"
:column="3"
border
>
<el-descriptions-item :span="3" :width="150">
<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>
{{ dateFormat(license.expired_at) }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">用户人数</div>
</template>
{{ license.user_num }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">机器码</div>
</template>
{{ license.machine_id }}
</el-descriptions-item>
</el-descriptions>
<el-form :model="system" label-width="150px" label-position="right">
<el-form-item label="许可授权码" prop="license">
<el-input v-model="licenseKey"/>
</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>
@@ -271,6 +317,7 @@ import {InfoFilled, UploadFilled} from "@element-plus/icons-vue";
import MdEditor from "md-editor-v3";
import 'md-editor-v3/lib/style.css';
import Menu from "@/views/admin/Menu.vue";
import {dateFormat} from "@/utils/libs";
const activeName = ref('basic')
const system = ref({models: []})
@@ -279,6 +326,7 @@ const systemFormRef = ref(null)
const models = ref([])
const openAIModels = ref([])
const notice = ref("")
const license = ref({is_active: false})
onMounted(() => {
// 加载系统配置
@@ -302,8 +350,17 @@ onMounted(() => {
ElMessage.error("获取模型失败:" + e.message)
})
fetchLicense()
})
const fetchLicense = () => {
httpGet("/api/admin/config/get/license").then(res => {
license.value = res.data
}).catch(e => {
ElMessage.error("获取 License 失败:" + e.message)
})
}
const rules = reactive({
title: [{required: true, message: '请输入网站标题', trigger: 'blur',}],
admin_title: [{required: true, message: '请输入控制台标题', trigger: 'blur',}],
@@ -331,6 +388,20 @@ const save = function (key) {
}
}
// 激活授权
const licenseKey = ref("")
const active = () => {
if (licenseKey.value === "") {
return ElMessage.error("请输入授权码")
}
httpPost("/api/admin/active", {license: licenseKey.value}).then(res => {
ElMessage.success("授权成功,机器编码为:" + res.data)
fetchLicense()
}).catch(e => {
ElMessage.error(e.message)
})
}
const configKey = ref("")
const beforeUpload = (key) => {
configKey.value = key
@@ -375,10 +446,9 @@ const onUploadImg = (files, callback) => {
}).catch(e => {
ElMessage.error('图片上传失败:' + e.message)
})
};
</script>
<style lang="stylus" scoped>
@@ -389,10 +459,9 @@ const onUploadImg = (files, callback) => {
.el-tabs {
width 100%
background-color #ffffff
background-color var(--el-bg-color)
padding 10px 20px 40px 20px
border: 1px solid #ddd;
border-radius: 5px
border: 1px solid var(--el-border-color);
.container {
.el-form {
@@ -425,6 +494,10 @@ const onUploadImg = (files, callback) => {
}
}
.el-descriptions {
margin-bottom 20px
}
.el-alert {
margin-bottom 15px;
}

View File

@@ -73,7 +73,7 @@
<el-input v-model="user.username" autocomplete="off"/>
</el-form-item>
<el-form-item v-if="add" label="密码" prop="password">
<el-input v-model="user.password" autocomplete="off"/>
<el-input v-model="user.password" autocomplete="off" placeholder="8-16"/>
</el-form-item>
<el-form-item label="剩余算力" prop="power">
<el-input v-model.number="user.power" autocomplete="off" placeholder="0"/>
@@ -186,8 +186,17 @@ const models = ref([])
const showUserEditDialog = ref(false)
const showResetPassDialog = ref(false)
const rules = reactive({
username: [{required: true, message: '请输入账号', trigger: 'change',}],
password: [{required: true, message: '请输入密码', trigger: 'change',}],
username: [{required: true, message: '请输入账号', trigger: 'blur',}],
password: [
{
required: true,
validator: (rule, value) => {
return !(value.length > 16 || value.length < 8);
}, message: '密码必须为8-16',
trigger: 'blur'
}
],
calls: [
{required: true, message: '请输入提问次数'},
{type: 'number', message: '请输入有效数字'},
@@ -323,6 +332,7 @@ const doResetPass = () => {
.user-list {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
@@ -341,6 +351,11 @@ const doResetPass = () => {
}
}
.pagination {
padding 20px 0
display flex
}
.el-select {
width: 100%
}

View File

@@ -1,18 +1,16 @@
<template>
<div class="chat-export-mobile">
<div class="chat-box">
<van-sticky :offset-top="0" position="top">
<van-nav-bar left-arrow left-text="返回" @click-left="router.back()">
<template #title>
<van-dropdown-menu>
<van-dropdown-item :title="title">
<van-cell center title="角色"> {{ role }}</van-cell>
<van-cell center title="模型">{{ model }}</van-cell>
</van-dropdown-item>
</van-dropdown-menu>
</template>
</van-nav-bar>
</van-sticky>
<van-nav-bar left-arrow left-text="返回" @click-left="router.back()">
<template #title>
<van-dropdown-menu>
<van-dropdown-item :title="title">
<van-cell center title="角色"> {{ role }}</van-cell>
<van-cell center title="模型">{{ model }}</van-cell>
</van-dropdown-item>
</van-dropdown-menu>
</template>
</van-nav-bar>
<div class="chat-list-wrapper">
<div id="message-list-box" class="message-list-box">
@@ -53,13 +51,14 @@ import {useRouter} from "vue-router";
import {httpGet} from "@/utils/http";
import 'highlight.js/styles/a11y-dark.css'
import hl from "highlight.js";
import {showFailToast} from "vant";
const chatData = ref([])
const router = useRouter()
const chatId = router.currentRoute.value.query['chat_id']
const title = router.currentRoute.value.query['title']
const role = router.currentRoute.value.query['role']
const model = router.currentRoute.value.query['model']
const title = ref('')
const role = ref('')
const model = ref('')
const finished = ref(false)
const error = ref(false)
@@ -117,28 +116,42 @@ const onLoad = () => {
error.value = true
})
httpGet(`/api/chat/detail?chat_id=${chatId}`).then(res => {
title.value = res.data.title
model.value = res.data.model
role.value = res.data.role_name
}).catch(e => {
showFailToast('加载对话失败:' + e.message)
})
};
</script>
<style lang="stylus">
.chat-export-mobile {
background #F5F5F5;
height 100vh
.van-nav-bar {
position static
}
.chat-box {
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
background #F5F5F5;
.message-list-box {
background #F5F5F5;
padding-top 50px
padding-bottom: 10px
.chat-list-wrapper {
padding 10px 0 10px 0
.van-cell {
background none
.message-list-box {
background #F5F5F5;
padding-bottom: 10px
.van-cell {
background none
}
}
}
.van-nav-bar__title {
.van-dropdown-menu__title {
margin-right 10px

View File

@@ -1,6 +1,6 @@
<template>
<div class="app-background">
<div v-if="isLogin" class="container mobile-chat-list">
<div class="container mobile-chat-list">
<van-nav-bar
:title="title"
left-text="新建会话"
@@ -82,8 +82,7 @@ import {httpGet, httpPost} from "@/utils/http";
import {showConfirmDialog, showFailToast, showSuccessToast} from "vant";
import {checkSession} from "@/action/session";
import {router} from "@/router";
import {setChatConfig} from "@/store/chat";
import {removeArrayItem} from "@/utils/libs";
import {removeArrayItem, showLoginDialog} from "@/utils/libs";
const title = ref("会话列表")
const chatName = ref("")
@@ -136,32 +135,56 @@ checkSession().then((user) => {
})
}).catch(() => {
router.push("/login")
loading.value = false
finished.value = true
// 加载角色列表
httpGet('/api/role/list').then((res) => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
// console.log(items[i])
roles.value.push({
text: items[i].name,
value: items[i].id,
icon: items[i].icon,
helloMsg: items[i].hello_msg
})
}
}
}).catch(() => {
showFailToast("加载聊天角色失败")
})
// 加载模型
httpGet('/api/model/list').then(res => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
models.value.push({text: items[i].name, value: items[i].id})
}
}
}).catch(e => {
showFailToast("加载模型失败: " + e.message)
})
})
const onLoad = () => {
httpGet("/api/chat/list?user_id=" + loginUser.value.id).then((res) => {
if (res.data) {
chats.value = res.data;
allChats.value = res.data;
finished.value = true
}
loading.value = false;
}).catch(() => {
error.value = true
showFailToast("加载会话列表失败")
checkSession().then(() => {
httpGet("/api/chat/list?user_id=" + loginUser.value.id).then((res) => {
if (res.data) {
chats.value = res.data;
allChats.value = res.data;
finished.value = true
}
loading.value = false;
}).catch(() => {
error.value = true
showFailToast("加载会话列表失败")
})
})
};
const getModelValue = (model_id) => {
for (let i = 0; i < models.value.length; i++) {
if (models.value[i].value === model_id) {
return models.value[i].text
}
}
return ""
}
const search = () => {
if (chatName.value === '') {
chats.value = allChats.value
@@ -177,6 +200,10 @@ const search = () => {
}
const clearAllChatHistory = () => {
if (!isLogin.value) {
return showLoginDialog(router)
}
showConfirmDialog({
title: '操作提示',
message: '确定要删除所有的会话记录吗?'
@@ -193,44 +220,50 @@ const clearAllChatHistory = () => {
}
const newChat = (item) => {
if (!isLogin.value) {
return showLoginDialog(router)
}
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
// 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}
})
router.push('/mobile/chat/session')
}
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')
// 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}`)
}
const editChat = (row) => {

View File

@@ -1,23 +1,25 @@
<template>
<div class="app-background">
<div class="mobile-chat" v-loading="loading" element-loading-text="正在连接会话...">
<van-sticky ref="navBarRef" :offset-top="0" position="top">
<van-nav-bar left-arrow left-text="返回" @click-left="router.back()">
<template #title>
<van-dropdown-menu>
<van-dropdown-item :title="title">
<van-cell center title="角色"> {{ role.name }}</van-cell>
<van-cell center title="模型">{{ modelValue }}</van-cell>
</van-dropdown-item>
</van-dropdown-menu>
</template>
<van-nav-bar ref="navBarRef">
<template #title>
<van-dropdown-menu>
<van-dropdown-item :title="title">
<van-cell center title="角色"> {{ role.name }}</van-cell>
<van-cell center title="模型">{{ modelValue }}</van-cell>
</van-dropdown-item>
</van-dropdown-menu>
</template>
<template #left>
<span class="setting">
<van-icon name="setting-o" @click="showPicker = true"/>
</span>
</template>
<template #right>
<van-icon name="share-o" @click="showShare = true"/>
</template>
<template #right>
<van-icon name="share-o" @click="showShare = true"/>
</template>
</van-nav-bar>
</van-sticky>
</van-nav-bar>
<van-share-sheet
v-model:show="showShare"
@@ -38,23 +40,19 @@
<chat-prompt
v-if="item.type==='prompt'"
:content="item.content"
:created-at="dateFormat(item['created_at'])"
:icon="item.icon"
:model="model"
:tokens="item['tokens']"/>
:icon="item.icon"/>
<chat-reply v-else-if="item.type==='reply'"
:content="item.content"
:created-at="dateFormat(item['created_at'])"
:icon="item.icon"
:org-content="item.orgContent"
:tokens="item['tokens']"/>
:org-content="item.orgContent"/>
</van-cell>
</van-list>
</div>
</div>
<div class="chat-box-wrapper">
<van-sticky ref="bottomBarRef" :offset-bottom="0" position="bottom">
<van-cell-group inset>
<van-field
v-model="prompt"
@@ -62,13 +60,13 @@
clearable
placeholder="输入你的问题"
>
<template #left-icon>
<van-button round type="success" class="button-voice" @click="inputVoice">
<el-icon>
<Microphone/>
</el-icon>
</van-button>
</template>
<!-- <template #left-icon>-->
<!-- <van-button round type="success" class="button-voice" @click="inputVoice">-->
<!-- <el-icon>-->
<!-- <Microphone/>-->
<!-- </el-icon>-->
<!-- </van-button>-->
<!-- </template>-->
<template #button>
<van-button size="small" type="primary" @click="sendMessage">发送</van-button>
@@ -87,27 +85,47 @@
<button id="copy-link-btn" style="display: none;" :data-clipboard-text="url">复制链接地址</button>
<van-overlay :show="showMic" z-index="100">
<div class="mic-wrapper">
<div class="image">
<van-image
width="100"
height="100"
src="/images/mic.gif"
/>
</div>
<van-button type="success" @click="stopVoice">说完了</van-button>
</div>
</van-overlay>
<!-- <van-overlay :show="showMic" z-index="100">-->
<!-- <div class="mic-wrapper">-->
<!-- <div class="image">-->
<!-- <van-image-->
<!-- width="100"-->
<!-- height="100"-->
<!-- src="/images/mic.gif"-->
<!-- />-->
<!-- </div>-->
<!-- <van-button type="success" @click="stopVoice">说完了</van-button>-->
<!-- </div>-->
<!-- </van-overlay>-->
</div>
<van-popup v-model:show="showPicker" position="bottom" class="popup">
<van-picker
:columns="columns"
title="选择模型和角色"
@cancel="showPicker = false"
@confirm="newChat"
>
<template #option="item">
<div class="picker-option">
<van-image
v-if="item.icon"
:src="item.icon"
fit="cover"
round
/>
<span>{{ item.text }}</span>
</div>
</template>
</van-picker>
</van-popup>
</template>
<script setup>
import {nextTick, onMounted, onUnmounted, ref} from "vue";
import {showImagePreview, showNotify, showToast} from "vant";
import {onBeforeRouteLeave, useRouter} from "vue-router";
import {dateFormat, processContent, randString, renderInputText, UUID} from "@/utils/libs";
import {getChatConfig} from "@/store/chat";
import {processContent, randString, renderInputText, UUID} from "@/utils/libs";
import {httpGet} from "@/utils/http";
import hl from "highlight.js";
import 'highlight.js/styles/a11y-dark.css'
@@ -116,26 +134,85 @@ import ChatReply from "@/components/mobile/ChatReply.vue";
import {getSessionId, getUserToken} from "@/store/session";
import {checkSession} from "@/action/session";
import Clipboard from "clipboard";
import {Microphone} from "@element-plus/icons-vue";
import {showLoginDialog} from "@/utils/dialog";
const winHeight = ref(0)
const navBarRef = ref(null)
const bottomBarRef = ref(null)
const router = useRouter()
const chatConfig = getChatConfig()
const role = chatConfig.role
const model = chatConfig.model
const modelValue = chatConfig.modelValue
const title = chatConfig.title
const chatId = chatConfig.chatId
const roles = ref([])
const roleId = ref(parseInt(router.currentRoute.value.query["role_id"]))
const role = ref({})
const models = ref([])
const modelId = ref(parseInt(router.currentRoute.value.query["model_id"]))
const modelValue = ref("")
const title = ref(router.currentRoute.value.query["title"])
const chatId = ref(router.currentRoute.value.query["chat_id"])
const loginUser = ref(null)
const showMic = ref(false)
// const showMic = ref(false)
const showPicker = ref(false)
const columns = ref([roles.value, models.value])
const url = location.protocol + '//' + location.host + '/mobile/chat/export?chat_id=' + chatId
checkSession().then(user => {
loginUser.value = user
}).catch(() => {
router.push('/login')
})
if (chatId.value) {
httpGet(`/api/chat/detail?chat_id=${chatId.value}`).then(res => {
title.value = res.data.title
modelId.value = res.data.model_id
roleId.value = res.data.role_id
}).catch(() => {
})
} else {
title.value = "新建对话"
}
// 加载模型
httpGet('/api/model/list').then(res => {
models.value = res.data
if (!modelId.value) {
modelId.value = models.value[0].id
}
for (let i = 0; i < models.value.length; i++) {
models.value[i].text = models.value[i].name
models.value[i].mValue = models.value[i].value
models.value[i].value = models.value[i].id
}
modelValue.value = getModelValue(modelId.value)
// 加载角色列表
httpGet(`/api/role/list`).then((res) => {
roles.value = res.data;
if (!roleId.value) {
roleId.value = roles.value[0]['id']
}
// build data for role picker
for (let i = 0; i < roles.value.length; i++) {
roles.value[i].text = roles.value[i].name
roles.value[i].value = roles.value[i].id
roles.value[i].helloMsg = roles.value[i].hello_msg
}
role.value = getRoleById(roleId.value)
columns.value = [roles.value, models.value]
// 新建对话
if (!chatId.value) {
connect(chatId.value, roleId.value, modelId.value)
}
}).catch((e) => {
showNotify({type: "danger", message: '获取聊天角色失败: ' + e.messages})
})
}).catch(e => {
showNotify({type: "danger", message: "加载模型失败: " + e.message})
})
const url = ref(location.protocol + '//' + location.host + '/mobile/chat/export?chat_id=' + chatId.value)
onMounted(() => {
winHeight.value = document.body.offsetHeight - navBarRef.value.$el.offsetHeight - bottomBarRef.value.$el.offsetHeight
winHeight.value = window.innerHeight - navBarRef.value.$el.offsetHeight - bottomBarRef.value.$el.offsetHeight - 70
const clipboard = new Clipboard(".content-mobile,.copy-code-mobile,#copy-link-btn");
clipboard.on('success', (e) => {
@@ -148,20 +225,29 @@ onMounted(() => {
})
onUnmounted(() => {
socket.value = null
if (socket.value !== null) {
socket.value.close()
socket.value = null
}
})
const newChat = (item) => {
showPicker.value = false
const options = item.selectedOptions
roleId.value = options[0].value
modelId.value = options[1].value
chatId.value = ""
chatData.value = []
role.value = getRoleById(roleId.value)
title.value = "新建对话"
connect(chatId.value, roleId.value, modelId.value)
}
const chatData = ref([])
const loading = ref(false)
const finished = ref(false)
const error = ref(false)
checkSession().then(user => {
loginUser.value = user
}).catch(() => {
router.push('/login')
})
const latexPlugin = require('markdown-it-latex2img')
const mathjaxPlugin = require('markdown-it-mathjax')
const md = require('markdown-it')({
@@ -193,39 +279,41 @@ md.use(mathjaxPlugin)
const onLoad = () => {
httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
// 加载状态结束
finished.value = true;
const data = res.data
if (data && data.length > 0) {
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
chatData.value.push(data[i]);
continue;
if (chatId.value) {
checkSession().then(() => {
httpGet('/api/chat/history?chat_id=' + chatId.value).then(res => {
// 加载状态结束
finished.value = true;
const data = res.data
if (data && data.length > 0) {
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
chatData.value.push(data[i]);
continue;
}
data[i].orgContent = data[i].content;
data[i].content = md.render(processContent(data[i].content))
chatData.value.push(data[i]);
}
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const blocks = document.querySelector("#message-list-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
scrollListBox()
})
}
data[i].orgContent = data[i].content;
data[i].content = md.render(processContent(data[i].content))
chatData.value.push(data[i]);
}
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const blocks = document.querySelector("#message-list-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
scrollListBox()
connect(chatId.value, roleId.value, modelId.value);
}).catch(() => {
error.value = true
})
}
// 连接会话
connect(chatId, role.id);
}).catch(() => {
error.value = true
})
}).catch(() => {
})
}
};
// 离开页面时主动关闭 websocket 连接,节省网络资源
@@ -248,7 +336,7 @@ const socket = ref(null);
const activelyClose = ref(false); // 主动关闭
const canSend = ref(true);
const heartbeatHandle = ref(null)
const connect = function (chat_id, role_id) {
const connect = function (chat_id, role_id, model_id) {
let isNewChat = false;
if (!chat_id) {
isNewChat = true;
@@ -278,7 +366,7 @@ const connect = function (chat_id, role_id) {
}
}
const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model_id=${model}&token=${getUserToken()}`);
const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model_id=${model_id}&token=${getUserToken()}`);
_socket.addEventListener('open', () => {
loading.value = false
previousText.value = '';
@@ -289,9 +377,9 @@ const connect = function (chat_id, role_id) {
chatData.value.push({
type: "reply",
id: randString(32),
icon: role.icon,
content: role.helloMsg,
orgContent: role.helloMsg,
icon: role.value.icon,
content: role.value.hello_msg,
orgContent: role.value.hello_msg,
})
}
@@ -309,9 +397,12 @@ const connect = function (chat_id, role_id) {
chatData.value.push({
type: "reply",
id: randString(32),
icon: role.icon,
icon: role.value.icon,
content: ""
});
if (isNewChat) {
title.value = previousText.value
}
} else if (data.type === 'end') { // 消息接收完毕
enableInput()
lineBuffer.value = ''; // 清空缓冲
@@ -358,10 +449,9 @@ const connect = function (chat_id, role_id) {
canSend.value = true;
// 重连
checkSession().then(() => {
connect(chat_id, role_id)
connect(chat_id, role_id, model_id)
}).catch(() => {
loading.value = true
setTimeout(() => connect(chat_id, role_id), 3000)
showLoginDialog(router)
});
});
@@ -447,38 +537,56 @@ const shareChat = (option) => {
showToast({message: "当前会话已经导出,请通过浏览器或者微信的自带分享功能分享给好友", duration: 5000})
router.push({
path: "/mobile/chat/export",
query: {title: title, chat_id: chatId, role: role.name, model: modelValue}
query: {title: title, chat_id: chatId, role: role.value.name, model: modelValue}
})
} else if (option.icon === "link") {
document.getElementById('copy-link-btn').click();
}
}
// eslint-disable-next-line no-undef
const recognition = new webkitSpeechRecognition() || SpeechRecognition();
//recognition.lang = 'zh-CN' // 设置语音识别语言
recognition.onresult = function (event) {
prompt.value = event.results[0][0].transcript
};
recognition.onerror = function (event) {
showMic.value = false
recognition.stop()
showNotify({type: 'danger', message: '语音识别错误:' + event.error})
};
recognition.onend = function () {
console.log('语音识别结束');
};
const inputVoice = () => {
showMic.value = true
recognition.start();
const getRoleById = function (rid) {
for (let i = 0; i < roles.value.length; i++) {
if (roles.value[i]['id'] === rid) {
return roles.value[i];
}
}
return null;
}
const stopVoice = () => {
showMic.value = false
recognition.stop()
const getModelValue = (model_id) => {
for (let i = 0; i < models.value.length; i++) {
if (models.value[i].id === model_id) {
return models.value[i].mValue
}
}
return ""
}
// // eslint-disable-next-line no-undef
// const recognition = new webkitSpeechRecognition() || SpeechRecognition();
// //recognition.lang = 'zh-CN' // 设置语音识别语言
// recognition.onresult = function (event) {
// prompt.value = event.results[0][0].transcript
// };
//
// recognition.onerror = function (event) {
// showMic.value = false
// recognition.stop()
// showNotify({type: 'danger', message: '语音识别错误:' + event.error})
// };
//
// recognition.onend = function () {
// console.log('语音识别结束');
// };
// const inputVoice = () => {
// showMic.value = true
// recognition.start();
// }
//
// const stopVoice = () => {
// showMic.value = false
// recognition.stop()
// }
</script>
<style lang="stylus">

View File

@@ -1,13 +1,14 @@
<template>
<van-config-provider :theme="getMobileTheme()">
<van-config-provider :theme="theme">
<div class="mobile-home">
<router-view/>
<van-tabbar route v-model="active" @change="onChange">
<van-tabbar-item to="/mobile/chat" name="home" icon="chat-o">对话</van-tabbar-item>
<van-tabbar route v-model="active">
<van-tabbar-item to="/mobile/index" name="home" icon="home-o">首页</van-tabbar-item>
<van-tabbar-item to="/mobile/chat" name="chat" icon="chat-o">对话</van-tabbar-item>
<van-tabbar-item to="/mobile/image" name="image" icon="photo-o">绘图</van-tabbar-item>
<van-tabbar-item to="/mobile/img-wall" name="apps" icon="apps-o">广场</van-tabbar-item>
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">我的</van-tabbar-item>
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">我的
</van-tabbar-item>
</van-tabbar>
</div>
@@ -17,24 +18,23 @@
<script setup>
import {ref} from "vue";
import {getMobileTheme} from "@/store/system";
import {getMobileTheme, setMobileTheme} from "@/store/system";
import {useRouter} from "vue-router";
import {isMobile} from "@/utils/libs";
import {checkSession} from "@/action/session";
import bus from '@/store/eventbus'
const router = useRouter()
checkSession().then(() => {
if (!isMobile()) {
router.replace('/chat')
}
}).catch(() => {
router.push('/login')
})
if (!isMobile()) {
router.replace('/')
}
const active = ref('home')
const onChange = (index) => {
console.log(index)
}
const theme = ref(getMobileTheme())
bus.on('changeTheme', (value) => {
theme.value = value
setMobileTheme(theme.value)
})
</script>
@@ -47,9 +47,7 @@ const onChange = (index) => {
width 100%
}
.content {
padding 46px 10px 60px 10px
}
padding 0 10px
}
}
@@ -59,16 +57,6 @@ const onChange = (index) => {
background #1c1c1e
}
.van-toast--fail {
background #fef0f0
color #f56c6c
}
.van-toast--success {
background #D6FBCC
color #07C160
}
.van-nav-bar {
position fixed
width 100%

View File

@@ -1,13 +1,13 @@
<template>
<div class="mobile-image container">
<van-tabs v-model:active="activeName" class="my-tab" animated sticky>
<van-tab title="MidJourney" name="mj">
<van-tab title="MJ" name="mj">
<image-mj/>
</van-tab>
<van-tab title="Stable-Diffusion" name="sd">
<van-tab title="SD" name="sd">
<image-sd/>
</van-tab>
<van-tab title="DALL-E" name="dall">
<van-tab title="DALL" name="dall">
<van-empty description="功能正在开发中"/>
</van-tab>
</van-tabs>
@@ -16,8 +16,8 @@
<script setup>
import {ref} from "vue";
import ImageMj from "@/views/mobile/ImageMj.vue";
import ImageSd from "@/views/mobile/ImageSd.vue";
import ImageMj from "@/views/mobile/pages/ImageMj.vue";
import ImageSd from "@/views/mobile/pages/ImageSd.vue";
const activeName = ref("mj")
</script>

View File

@@ -0,0 +1,235 @@
<template>
<div class="index container">
<h2 class="title">{{title}}</h2>
<van-notice-bar left-icon="info-o" :scrollable="true">
你有多少想象力AI就有多大创造力我辈之人先干为敬陪您先把 AI 用起来
</van-notice-bar>
<div class="content">
<van-grid :column-num="3" :gutter="10" border>
<van-grid-item @click="router.push('chat')">
<template #icon>
<i class="iconfont icon-chat"></i>
</template>
<template #text>
<div class="text">AI 对话</div>
</template>
</van-grid-item>
<van-grid-item @click="router.push('image')">
<template #icon>
<i class="iconfont icon-mj"></i>
</template>
<template #text>
<div class="text">AI 绘画</div>
</template>
</van-grid-item>
<van-grid-item @click="router.push('imgWall')">
<template #icon>
<van-icon name="photo-o" />
</template>
<template #text>
<div class="text">AI 画廊</div>
</template>
</van-grid-item>
</van-grid>
<div class="app-list">
<van-list
v-model:loading="loading"
:finished="true"
finished-text=""
@load="fetchApps"
>
<van-cell v-for="item in apps" :key="item.id">
<div>
<div class="item">
<div class="image">
<van-image :src="item.icon" />
</div>
<div class="info">
<div class="info-title">{{item.name}}</div>
<div class="info-text">{{item.hello_msg}}</div>
</div>
</div>
<div class="btn">
<div v-if="hasRole(item.key)">
<van-button size="small" type="success" @click="useRole(item.id)">使用</van-button>
<van-button size="small" type="danger" @click="updateRole(item,'remove')">移除</van-button>
</div>
<van-button v-else size="small"
style="--el-color-primary:#009999"
@click="updateRole(item, 'add')">
<van-icon name="add-o" />
<span>添加应用</span>
</van-button>
</div>
</div>
</van-cell>
</van-list>
</div>
</div>
</div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {useRouter} from "vue-router";
import {checkSession} from "@/action/session";
import {httpGet, httpPost} from "@/utils/http";
import {arrayContains, removeArrayItem, showLoginDialog, substr} from "@/utils/libs";
import {showNotify} from "vant";
import {ElMessage} from "element-plus";
const title = ref(process.env.VUE_APP_TITLE)
const router = useRouter()
const isLogin = ref(false)
const apps = ref([])
const loading = ref(false)
const roles = ref([])
onMounted(() => {
checkSession().then((user) => {
isLogin.value = true
roles.value = user.chat_roles
}).catch(() => {
})
fetchApps()
})
const fetchApps = () => {
httpGet("/api/role/list?all=true").then((res) => {
const items = res.data
// 处理 hello message
for (let i = 0; i < items.length; i++) {
items[i].intro = substr(items[i].hello_msg, 80)
}
apps.value = items
}).catch(e => {
showNotify({type:"danger", message:"获取应用失败:" + e.message})
})
}
const updateRole = (row, opt) => {
if (!isLogin.value) {
return showLoginDialog(router)
}
const title = ref("")
if (opt === "add") {
title.value = "添加应用"
const exists = arrayContains(roles.value, row.key)
if (exists) {
return
}
roles.value.push(row.key)
} else {
title.value = "移除应用"
const exists = arrayContains(roles.value, row.key)
if (!exists) {
return
}
roles.value = removeArrayItem(roles.value, row.key)
}
httpPost("/api/role/update", {keys: roles.value}).then(() => {
ElMessage.success({message: title.value + "成功!", duration: 1000})
}).catch(e => {
ElMessage.error(title.value + "失败:" + e.message)
})
}
const hasRole = (roleKey) => {
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
}
const useRole = (roleId) => {
if (!isLogin.value) {
return showLoginDialog(router)
}
router.push(`/mobile/chat/session?role_id=${roleId}`)
}
</script>
<style scoped lang="stylus">
.index {
color var(--van-text-color)
.title {
display flex
justify-content center
}
--van-notice-bar-font-size: 16px
.content {
padding 15px 0 60px 0
.van-grid-item {
.iconfont {
font-size 20px
}
.text {
display flex
width 100%
padding 10px
justify-content center
font-size 14px
}
}
.app-list {
padding-top 10px
.item {
display flex
.image {
width 80px
height 80px
min-width 80px
border-radius 5px
overflow hidden
}
.info {
text-align left
padding 0 10px
.info-title {
color var(--van-text-color)
font-size 1.25rem
line-height 1.75rem
letter-spacing: .025em;
font-weight: 600;
word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.info-text {
padding 5px 0
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
word-break: break-all;
font-size: .875rem;
}
}
}
.btn {
padding 5px 0
.van-button {
margin-right 10px
.van-icon {
margin-right 5px
}
}
}
}
}
}
</style>

View File

@@ -1,43 +1,55 @@
<template>
<div class="mobile-user-profile container">
<van-nav-bar :title="title"/>
<div class="content">
<van-form>
<div class="avatar">
<van-uploader v-model="fileList"
reupload max-count="1"
:deletable="false"
:after-read="afterRead"/>
</div>
<van-cell-group inset v-model="form">
<van-field
v-model="form.username"
name="账号"
label="账号"
readonly
disabled
/>
<van-field label="头像">
<template #input>
<van-uploader v-model="fileList"
reupload max-count="1"
:deletable="false"
:after-read="afterRead"/>
</template>
</van-field>
<van-field
v-model="form.nickname"
label="昵称"
readonly
disabled
/>
<van-field label="剩余算力">
<van-field label="算力">
<template #input>
<van-tag type="primary">{{ form.power }}</van-tag>
</template>
</van-field>
<van-field label="VIP到期时间" v-if="form.expired_time > 0">
<van-field label="有效期" v-if="form.expired_time > 0">
<template #input>
<van-tag type="warning">{{ dateFormat(form.expired_time) }}</van-tag>
{{ dateFormat(form.expired_time) }}
</template>
</van-field>
</van-cell-group>
</van-form>
<div class="modify-pass">
<van-button round block type="primary" @click="showPasswordDialog = true">修改密码</van-button>
<div class="opt" v-if="isLogin">
<van-row :gutter="10">
<van-col :span="8">
<van-button round block @click="showPasswordDialog = true" size="small">修改密码</van-button>
</van-col>
<van-col :span="8">
<van-button round block @click="logout" size="small">退出登录</van-button>
</van-col>
<van-col :span="8">
<van-button round block @click="showSettings = true" icon="setting" size="small">设置</van-button>
</van-col>
</van-row>
</div>
<div class="product-list">
@@ -106,6 +118,34 @@
</van-cell-group>
</van-form>
</van-dialog>
<van-action-sheet v-model:show="showSettings" title="用户设置">
<div class="setting-content">
<van-form>
<van-cell-group inset>
<van-field name="switch" label="暗黑主题">
<template #input>
<van-switch v-model="dark" @change="changeTheme"/>
</template>
</van-field>
<!-- <van-field-->
<!-- v-model="password"-->
<!-- type="password"-->
<!-- name="密码"-->
<!-- label="密码"-->
<!-- placeholder="密码"-->
<!-- :rules="[{ required: true, message: '请填写密码' }]"-->
<!-- />-->
</van-cell-group>
<!-- <div style="margin: 16px;">-->
<!-- <van-button round block type="primary" native-type="submit">-->
<!-- 提交-->
<!-- </van-button>-->
<!-- </div>-->
</van-form>
</div>
</van-action-sheet>
</div>
</template>
@@ -114,23 +154,24 @@ import {onMounted, ref} from "vue";
import {showFailToast, showNotify, showSuccessToast} from "vant";
import {httpGet, httpPost} from "@/utils/http";
import Compressor from 'compressorjs';
import {dateFormat} from "@/utils/libs";
import {dateFormat, showLoginDialog} from "@/utils/libs";
import {ElMessage} from "element-plus";
import {checkSession} from "@/action/session";
import {useRouter} from "vue-router";
import {removeUserToken} from "@/store/session";
import bus from '@/store/eventbus'
import {getMobileTheme} from "@/store/system";
const title = ref('用户设置')
const form = ref({
username: '',
nickname: '',
mobile: '',
username: 'GeekMaster',
nickname: '极客学长@001',
mobile: '1300000000',
avatar: '',
calls: 0,
tokens: 0
power: 0,
})
const fileList = ref([
{
url: '',
url: '/images/user-info.png',
message: '上传中...',
}
]);
@@ -139,11 +180,14 @@ const products = ref([])
const vipMonthPower = ref(0)
const payWays = ref({})
const router = useRouter()
const loginUser = ref(null)
const userId = ref(0)
const isLogin = ref(false)
const showSettings = ref(false)
onMounted(() => {
checkSession().then(user => {
loginUser.value = user
userId.value = user.id
isLogin.value = true
httpGet('/api/user/profile').then(res => {
form.value = res.data
fileList.value[0].url = form.value.avatar
@@ -151,28 +195,27 @@ onMounted(() => {
console.log(e.message)
showFailToast('获取用户信息失败')
});
// 获取产品列表
httpGet("/api/product/list").then((res) => {
products.value = res.data
}).catch(e => {
showFailToast("获取产品套餐失败:" + e.message)
})
httpGet("/api/config/get?key=system").then(res => {
vipMonthPower.value = res.data['vip_month_power']
}).catch(e => {
showFailToast("获取系统配置失败:" + e.message)
})
httpGet("/api/payment/payWays").then(res => {
payWays.value = res.data
}).catch(e => {
ElMessage.error("获取支付方式失败:" + e.message)
})
}).catch(() => {
router.push("/login")
})
// 获取产品列表
httpGet("/api/product/list").then((res) => {
products.value = res.data
}).catch(e => {
showFailToast("获取产品套餐失败:" + e.message)
})
httpGet("/api/config/get?key=system").then(res => {
vipMonthPower.value = res.data['vip_month_power']
}).catch(e => {
showFailToast("获取系统配置失败:" + e.message)
})
httpGet("/api/payment/payWays").then(res => {
payWays.value = res.data
}).catch(e => {
ElMessage.error("获取支付方式失败:" + e.message)
})
})
@@ -244,10 +287,14 @@ const updatePass = () => {
}
const pay = (payWay, item) => {
if (!isLogin.value) {
return showLoginDialog(router)
}
httpPost("/api/payment/mobile", {
pay_way: payWay,
product_id: item.id,
user_id: loginUser.value.id
user_id: userId.value
}).then(res => {
// console.log(res.data)
location.href = res.data
@@ -255,25 +302,55 @@ const pay = (payWay, item) => {
showFailToast("生成支付订单失败:" + e.message)
})
}
const logout = function () {
httpGet('/api/user/logout').then(() => {
removeUserToken();
router.push('/');
}).catch(() => {
showFailToast('注销失败!');
})
}
const dark = ref(getMobileTheme() === 'dark')
const changeTheme = () => {
bus.emit('changeTheme', dark.value ? 'dark' : 'light')
}
</script>
<style lang="stylus">
.mobile-user-profile {
.content {
padding-top 15px
padding-bottom 60px
.avatar {
display flex
justify-content center
.van-image {
border-radius 50%
}
}
.van-field__label {
width 100px
text-align right
}
.modify-pass {
.opt {
padding 10px 15px
}
.product-list {
padding 0 15px
color var(--van-text-color)
.item {
border 1px solid #e5e5e5
border 1px solid var(--van-border-color)
border-radius 10px
margin-bottom 15px
overflow hidden
@@ -294,6 +371,10 @@ const pay = (payWay, item) => {
}
}
.van-cell__value {
flex 2
}
.price {
font-size 18px
color #f56c6c
@@ -301,5 +382,9 @@ const pay = (payWay, item) => {
}
}
}
.setting-content {
padding 16px
}
}
</style>

View File

@@ -161,7 +161,7 @@
</van-collapse>
</div>
<div class="text-line">
<div class="text-line pt-6">
<el-tag>绘图消耗{{ mjPower }}算力U/V 操作消耗{{ mjActionPower }}算力当前算力{{ power }}</el-tag>
</div>
@@ -270,26 +270,21 @@
</div>
<button style="display: none" class="copy-prompt" :data-clipboard-text="prompt" id="copy-btn">复制</button>
</div>
</template>
<script setup>
import {nextTick, onMounted, onUnmounted, ref} from "vue";
import {
showConfirmDialog,
showFailToast,
showNotify,
showToast,
showDialog,
showImagePreview,
showSuccessToast
} from "vant";
import {showConfirmDialog, showFailToast, showImagePreview, showNotify, showSuccessToast, showToast} from "vant";
import {httpGet, httpPost} from "@/utils/http";
import Compressor from "compressorjs";
import {getSessionId} from "@/store/session";
import {checkSession} from "@/action/session";
import {useRouter} from "vue-router";
import {Delete} from "@element-plus/icons-vue";
import {showLoginDialog} from "@/utils/libs";
import Clipboard from "clipboard";
const activeColspan = ref([""])
@@ -335,23 +330,38 @@ const finishedJobs = ref([])
const socket = ref(null)
const power = ref(0)
const activeName = ref("txt2img")
const isLogin = ref(false)
const prompt = ref('')
const clipboard = ref(null)
onMounted(() => {
clipboard.value = new Clipboard(".copy-prompt");
clipboard.value.on('success', () => {
showNotify({type: 'success', message: '复制成功', duration: 1000})
})
clipboard.value.on('error', () => {
showNotify({type: 'danger', message: '复制失败', duration: 2000})
})
checkSession().then(user => {
power.value = user['power']
userId.value = user.id
isLogin.value = true
fetchRunningJobs()
fetchFinishJobs(1)
connect()
}).catch(() => {
router.push('/login')
// router.push('/login')
});
})
onUnmounted(() => {
socket.value = null
clipboard.value.destroy()
if (socket.value !== null) {
socket.value.close()
socket.value = null
}
})
const mjPower = ref(1)
@@ -564,6 +574,10 @@ const variation = (index, item) => {
}
const generate = () => {
if (!isLogin.value) {
return showLoginDialog(router)
}
if (params.value.prompt === '' && params.value.task_type === "image") {
return showFailToast("请输入绘画提示词!")
}
@@ -610,11 +624,15 @@ const publishImage = (item, action) => {
}
const showPrompt = (item) => {
showDialog({
prompt.value = item.prompt
showConfirmDialog({
title: "绘画提示词",
message: item.prompt,
confirmButtonText: "复制",
cancelButtonText: "关闭",
}).then(() => {
// on close
document.querySelector('#copy-btn').click()
}).catch(() => {
});
}

View File

@@ -198,7 +198,7 @@
<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="showTask(item)" circle>
<el-button type="primary" @click="showPrompt(item)" circle>
<i class="iconfont icon-prompt"></i>
</el-button>
</div>
@@ -208,7 +208,7 @@
</van-list>
</div>
<button style="display: none" class="copy-prompt-sd" :data-clipboard-text="prompt" id="copy-btn-sd">复制</button>
</div>
</template>
@@ -221,19 +221,19 @@ import {checkSession} from "@/action/session";
import {useRouter} from "vue-router";
import {getSessionId} from "@/store/session";
import {
showConfirmDialog, showDialog,
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 showTaskDialog = ref(false)
const item = ref({})
const showLoginDialog = ref(false)
const isLogin = ref(false)
const activeColspan = ref([""])
@@ -338,15 +338,15 @@ const connect = () => {
}
const clipboard = ref(null)
const prompt = ref('')
onMounted(() => {
initData()
clipboard.value = new Clipboard('.copy-prompt-sd');
clipboard.value = new Clipboard(".copy-prompt-sd");
clipboard.value.on('success', () => {
showNotify({type: "success", message: "复制成功"});
showNotify({type: 'success', message: '复制成功', duration: 1000})
})
clipboard.value.on('error', () => {
showNotify({type: "danger", message: '复制失败'});
showNotify({type: 'danger', message: '复制失败', duration: 2000})
})
httpGet("/api/config/get?key=system").then(res => {
@@ -358,7 +358,10 @@ onMounted(() => {
onUnmounted(() => {
clipboard.value.destroy()
socket.value = null
if (socket.value !== null) {
socket.value.close()
socket.value = null
}
})
@@ -429,17 +432,16 @@ const onLoad = () => {
//
const promptRef = ref(null)
const generate = () => {
if (!isLogin.value) {
return showLoginDialog(router)
}
if (params.value.prompt === '') {
promptRef.value.focus()
return showToast("请输入绘画提示词!")
}
if (!isLogin.value) {
showLoginDialog.value = true
return
}
if (params.value.seed === '') {
if (!params.value.seed) {
params.value.seed = -1
}
params.value.session_id = getSessionId()
@@ -451,14 +453,17 @@ const generate = () => {
})
}
const showTask = (row) => {
item.value = row
showTaskDialog.value = true
}
const copyParams = (row) => {
params.value = row.params
showTaskDialog.value = false
const showPrompt = (item) => {
prompt.value = item.prompt
showConfirmDialog({
title: "绘画提示词",
message: item.prompt,
confirmButtonText: "复制",
cancelButtonText: "关闭",
}).then(() => {
document.querySelector('#copy-btn-sd').click()
}).catch(() => {
});
}
const removeImage = (event, item) => {

View File

@@ -1,10 +1,8 @@
<template>
<div class="img-wall container">
<van-nav-bar :title="title"/>
<div class="content">
<van-tabs v-model:active="activeName">
<van-tab title="MidJourney" name="mj">
<van-tabs v-model:active="activeName" animated sticky>
<van-tab title="MJ" name="mj">
<van-list
v-model:error="data['mj'].error"
v-model:loading="data['mj'].loading"
@@ -15,11 +13,17 @@
style="height: 100%;width: 100%;"
>
<van-cell v-for="item in data['mj'].data" :key="item.id">
<van-image :src="item['img_thumb']" @click="showPrompt(item)" fit="cover"/>
<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-tab title="StableDiffusion" name="sd">
<van-tab title="SD" name="sd">
<van-list
v-model:error="data['sd'].error"
v-model:loading="data['sd'].loading"
@@ -29,25 +33,34 @@
@load="onLoad"
>
<van-cell v-for="item in data['sd'].data" :key="item.id">
<van-image :src="item['img_thumb']" @click="showPrompt(item)" fit="cover"/>
<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-tab title="DALLE3" name="dalle3">
<van-tab title="DALL" name="dalle3">
<van-empty description="功能正在开发中"/>
</van-tab>
</van-tabs>
</div>
<button style="display: none" class="copy-prompt-wall" :data-clipboard-text="prompt" id="copy-btn-wall">复制
</button>
</div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {showDialog, showFailToast, showSuccessToast} from "vant";
import {onMounted, onUnmounted, ref} from "vue";
import {httpGet} from "@/utils/http";
import {showConfirmDialog, showFailToast, showImagePreview, showNotify} from "vant";
import Clipboard from "clipboard";
import {ElMessage} from "element-plus";
const title = ref('图片创作广场')
const activeName = ref("mj")
const data = ref({
"mj": {
@@ -56,7 +69,7 @@ const data = ref({
error: false,
page: 1,
pageSize: 12,
url: "/api/mj/jobs",
url: "/api/mj/imgWall",
data: []
},
"sd": {
@@ -65,7 +78,7 @@ const data = ref({
error: false,
page: 1,
pageSize: 12,
url: "/api/sd/jobs",
url: "/api/sd/imgWall",
data: []
},
"dalle3": {
@@ -74,11 +87,32 @@ const data = ref({
error: false,
page: 1,
pageSize: 12,
url: "/api/dalle3/jobs",
url: "/api/dalle3/imgWall",
data: []
}
})
const prompt = ref('')
const clipboard = ref(null)
onMounted(() => {
clipboard.value = new Clipboard(".copy-prompt-wall");
clipboard.value.on('success', () => {
showNotify({type: 'success', message: '复制成功', duration: 1000})
})
clipboard.value.on('error', () => {
showNotify({type: 'danger', message: '复制失败', duration: 2000})
})
clipboard.value.on('error', () => {
ElMessage.error('复制失败!');
})
})
onUnmounted(() => {
clipboard.value.destroy()
})
const onLoad = () => {
const d = data.value[activeName.value]
httpGet(`${d.url}?status=1&page=${d.page}&page_size=${d.pageSize}&publish=true`).then(res => {
@@ -109,24 +143,39 @@ const onLoad = () => {
};
const showPrompt = (item) => {
showDialog({
prompt.value = item.prompt
showConfirmDialog({
title: "绘画提示词",
message: item.prompt,
confirmButtonText: "复制",
cancelButtonText: "关闭",
}).then(() => {
// on close
document.querySelector('#copy-btn-wall').click()
}).catch(() => {
});
}
const imageView = (item) => {
showImagePreview([item['img_url']]);
}
</script>
<style lang="stylus">
.img-wall {
.content {
padding-top 60px
.van-cell__value {
min-height 80px
.van-image {
width 100%
}
.opt-box {
position absolute
right 0
top 0
padding 10px
}
}
}
}