mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-05 16:53:46 +08:00
refactor: V3 版本重构已基本完成
This commit is contained in:
BIN
web/src/assets/img/login-bg.png
Normal file
BIN
web/src/assets/img/login-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 337 KiB |
BIN
web/src/assets/img/reg-bg-1.png
Normal file
BIN
web/src/assets/img/reg-bg-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
web/src/assets/img/reg-bg.png
Normal file
BIN
web/src/assets/img/reg-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -1,21 +1,32 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-right">
|
||||
<div class="chat-item">
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="triangle"></div>
|
||||
<div class="chat-line chat-line-prompt">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="icon" alt="User"/>
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="bar" v-if="createdAt !== ''">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
|
||||
<span class="bar-item">tokens: {{ finalTokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-icon">
|
||||
<img :src="icon" alt="User"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent} from "vue"
|
||||
import {dateFormat} from "@/utils/libs";
|
||||
import {Clock} from "@element-plus/icons-vue";
|
||||
import {httpGet} from "@/utils/http";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ChatPrompt',
|
||||
components: {Clock},
|
||||
methods: {dateFormat},
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
@@ -24,51 +35,106 @@ export default defineComponent({
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'images/user-icon.png',
|
||||
}
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
tokens: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
model: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
finalTokens: this.tokens
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.finalTokens) {
|
||||
httpGet(`/api/chat/tokens?text=${this.content}&model=${this.model}`).then(res => {
|
||||
this.finalTokens = res.data;
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.chat-line-right {
|
||||
justify-content: flex-end;
|
||||
<style lang="stylus" scoped>
|
||||
.chat-line-prompt {
|
||||
background-color #ffffff;
|
||||
justify-content: center;
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
|
||||
.chat-icon {
|
||||
margin-left 5px;
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
max-width 900px;
|
||||
padding-left 10px;
|
||||
|
||||
img {
|
||||
border-radius 5px;
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 10px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
|
||||
p {
|
||||
line-height 1.5
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
padding 10px;
|
||||
|
||||
.bar-item {
|
||||
background-color #f7f7f8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 5px 0 0;
|
||||
overflow: hidden;
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-left: 5px solid #98E165;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
background-color: #98E165;
|
||||
color var(--content-color);
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
line-height 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +1,22 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-left">
|
||||
<div class="chat-icon">
|
||||
<img :src="icon" alt="ChatGPT">
|
||||
</div>
|
||||
<div class="chat-line chat-line-reply">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="icon" alt="ChatGPT">
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="triangle"></div>
|
||||
<div class="content-box">
|
||||
<div class="chat-item">
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="tool-box">
|
||||
<div class="bar" v-if="createdAt !== ''">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
|
||||
<span class="bar-item">tokens: {{ tokens }}</span>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
effect="light"
|
||||
content="复制回答"
|
||||
placement="bottom"
|
||||
placement="top"
|
||||
>
|
||||
<el-button type="info" class="copy-reply" :data-clipboard-text="orgContent" plain>
|
||||
<el-button type="info" class="copy-reply" :data-clipboard-text="orgContent">
|
||||
<el-icon>
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
@@ -30,12 +31,11 @@
|
||||
|
||||
<script>
|
||||
import {defineComponent} from "vue"
|
||||
import {randString} from "@/utils/libs";
|
||||
import {DocumentCopy} from "@element-plus/icons-vue";
|
||||
import {Clock, DocumentCopy} from "@element-plus/icons-vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ChatReply',
|
||||
components: {DocumentCopy},
|
||||
components: {Clock, DocumentCopy},
|
||||
props: {
|
||||
content: {
|
||||
type: String,
|
||||
@@ -45,6 +45,14 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
createdAt: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
tokens: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'images/gpt-icon.png',
|
||||
@@ -52,73 +60,99 @@ export default defineComponent({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: randString(32),
|
||||
clipboard: null,
|
||||
finalTokens: this.tokens
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.chat-line-left {
|
||||
justify-content: flex-start;
|
||||
.common-layout {
|
||||
.chat-line-reply {
|
||||
justify-content: center;
|
||||
background-color: rgba(247, 247, 248, 1);
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
|
||||
.chat-icon {
|
||||
margin-right 5px;
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
width 100%;
|
||||
max-width 900px;
|
||||
padding-left 10px;
|
||||
|
||||
img {
|
||||
border-radius 5px;
|
||||
}
|
||||
}
|
||||
.chat-icon {
|
||||
margin-right 20px;
|
||||
|
||||
.chat-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 5px solid transparent;
|
||||
border-bottom: 5px solid transparent;
|
||||
border-right: 5px solid #fff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 13px;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
|
||||
display flex
|
||||
flex-direction row
|
||||
|
||||
.content {
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color var(--content-color)
|
||||
background-color: #fff;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
|
||||
p > code {
|
||||
color #cc0000
|
||||
background-color #f1f1f1
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 10px;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
position: relative;
|
||||
padding: 0 0 0 5px;
|
||||
overflow: hidden;
|
||||
|
||||
.content {
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 6px 10px;
|
||||
color #374151;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow auto;
|
||||
|
||||
p {
|
||||
line-height 1.5
|
||||
|
||||
code {
|
||||
color #f1f1f1
|
||||
background-color #202121
|
||||
padding 0 3px;
|
||||
border-radius 5px;
|
||||
}
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bar {
|
||||
padding 10px;
|
||||
|
||||
.bar-item {
|
||||
background-color #e7e7e8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
top 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-button {
|
||||
height 20px
|
||||
padding 5px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.tool-box {
|
||||
padding-left 10px;
|
||||
font-size 16px;
|
||||
|
||||
.el-button {
|
||||
|
||||
@@ -1,111 +1,57 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="$props.show"
|
||||
v-model="props.show"
|
||||
:close-on-click-modal="false"
|
||||
:show-close="true"
|
||||
:before-close="close"
|
||||
:top="top"
|
||||
title="聊天配置"
|
||||
title="用户设置"
|
||||
>
|
||||
<div class="user-info">
|
||||
<el-input :value="user['api_key']" placeholder="填写你 OpenAI 的 API KEY">
|
||||
<template #prepend>API KEY</template>
|
||||
</el-input>
|
||||
<div class="user-info" id="user-info">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="form['nickname']"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="头像">
|
||||
<el-input v-model="form['avatar']"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form['username']" disabled/>
|
||||
</el-form-item>
|
||||
|
||||
<el-descriptions
|
||||
class="margin-top"
|
||||
title="账户信息"
|
||||
:column="col"
|
||||
border
|
||||
>
|
||||
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
账户
|
||||
</div>
|
||||
</template>
|
||||
{{ user.name }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon>
|
||||
<List/>
|
||||
</el-icon>
|
||||
聊天记录
|
||||
</div>
|
||||
</template>
|
||||
<el-tag v-if="user['enable_history']" type="success">已开通</el-tag>
|
||||
<el-tag v-else type="info">未开通</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon>
|
||||
<Histogram/>
|
||||
</el-icon>
|
||||
总调用次数
|
||||
</div>
|
||||
</template>
|
||||
{{ user['max_calls'] }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon>
|
||||
<Histogram/>
|
||||
</el-icon>
|
||||
剩余点数
|
||||
</div>
|
||||
</template>
|
||||
{{ user["remaining_calls"] }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon>
|
||||
<Timer/>
|
||||
</el-icon>
|
||||
激活时间
|
||||
</div>
|
||||
</template>
|
||||
{{ user['active_time'] }}
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon>
|
||||
<Watch/>
|
||||
</el-icon>
|
||||
到期时间
|
||||
</div>
|
||||
</template>
|
||||
{{ user['expired_time'] }}
|
||||
</el-descriptions-item>
|
||||
|
||||
</el-descriptions>
|
||||
<el-form-item label="聊天上下文">
|
||||
<el-switch v-model="form['chat_config']['enable_context']"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="聊天记录">
|
||||
<el-switch v-model="form['chat_config']['enable_history']"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Model">
|
||||
<el-select v-model="form['chat_config']['model']" placeholder="默认会话模型">
|
||||
<el-option
|
||||
v-for="item in props.models"
|
||||
:key="item"
|
||||
:label="item.toUpperCase()"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="MaxTokens">
|
||||
<el-input v-model.number="form['chat_config']['max_tokens']"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Temperature">
|
||||
<el-input v-model.number="form['chat_config']['temperature']"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="剩余调用次数">
|
||||
<el-tag>{{ form['calls'] }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="剩余 Tokens">
|
||||
<el-tag type="info">{{ form['tokens'] }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="API KEY">
|
||||
<el-input v-model="form['chat_config']['api_key']"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<el-row class="row-center">
|
||||
<span>其他功能正在开发中,有什么使用建议可以通过下面的方式联系作者。</span>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-image :src="wechatGroup"></el-image>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-image :src="wechatCard"></el-image>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@@ -118,61 +64,54 @@
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent} from "vue"
|
||||
import {
|
||||
List, Timer, Watch,
|
||||
UserFilled,
|
||||
Histogram
|
||||
} from '@element-plus/icons-vue'
|
||||
import {isMobile} from "@/utils/libs";
|
||||
<script setup>
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ConfigDialog',
|
||||
components: {Watch, Timer, UserFilled, List, Histogram},
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
user: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
wechatGroup: "https://img.r9it.com/chatgpt/wechat-group.jpeg",
|
||||
wechatCard: "https://img.r9it.com/chatgpt/wechat-card.jpeg"
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
top: function () {
|
||||
if (window.innerHeight < 1000) {
|
||||
return '1vh';
|
||||
} else {
|
||||
return '15vh';
|
||||
}
|
||||
},
|
||||
|
||||
col: function () {
|
||||
return isMobile() ? 1 : 2;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
save: function () {
|
||||
this.$emit('update:show', false);
|
||||
},
|
||||
close: function () {
|
||||
this.$emit('update:show', false);
|
||||
}
|
||||
import {computed, defineEmits, defineProps, onMounted, ref} from "vue"
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
models: Array,
|
||||
});
|
||||
|
||||
const form = ref({})
|
||||
const top = computed(() => {
|
||||
if (window.innerHeight < 768) {
|
||||
return '1vh';
|
||||
} else {
|
||||
return '15vh';
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 获取最新用户信息
|
||||
httpGet('/api/user/profile').then(res => {
|
||||
form.value = res.data
|
||||
}).catch(() => {
|
||||
ElMessage.error('获取用户信息失败')
|
||||
});
|
||||
})
|
||||
|
||||
const emits = defineEmits(['update:show']);
|
||||
const save = function () {
|
||||
httpPost('/api/user/profile/update', form.value).then(() => {
|
||||
ElMessage.success({
|
||||
message: '更新成功',
|
||||
appendTo: document.getElementById('user-info'),
|
||||
onClose: () => emits('update:show', false)
|
||||
})
|
||||
}).catch(() => {
|
||||
ElMessage.error({
|
||||
message: '更新失败',
|
||||
appendTo: document.getElementById('user-info')
|
||||
})
|
||||
})
|
||||
}
|
||||
const close = function () {
|
||||
emits('update:show', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@@ -182,17 +121,15 @@ export default defineComponent({
|
||||
|
||||
.el-dialog__body {
|
||||
padding-top 10px;
|
||||
max-height 600px;
|
||||
overflow-y auto;
|
||||
|
||||
.user-info {
|
||||
.margin-top {
|
||||
margin-top 20px;
|
||||
}
|
||||
position relative;
|
||||
|
||||
.el-icon {
|
||||
top 2px;
|
||||
.el-message {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
margin-bottom 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +52,9 @@ export default defineComponent({
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid transparent;
|
||||
border-left: 10px solid #223A34;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
border-left: 6px solid #223A34;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 10px;
|
||||
@@ -62,11 +62,12 @@ export default defineComponent({
|
||||
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 12px 15px;
|
||||
padding: 6px 10px;
|
||||
background-color: #223A34;
|
||||
color var(--content-color);
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
|
||||
p {
|
||||
line-height 1.5
|
||||
@@ -55,7 +55,7 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.body-plus {
|
||||
.common-layout {
|
||||
.chat-line-left {
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -76,12 +76,12 @@ export default defineComponent({
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid transparent;
|
||||
border-right: 10px solid #404042;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
border-right: 6px solid #404042;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 13px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
@@ -92,11 +92,12 @@ export default defineComponent({
|
||||
.content {
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 12px 15px;
|
||||
padding: 6px 10px;
|
||||
color var(--content-color)
|
||||
background-color: #404042;
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow auto;
|
||||
|
||||
p {
|
||||
line-height 1.5
|
||||
@@ -1,15 +1,15 @@
|
||||
import {createRouter, createWebHashHistory} from 'vue-router'
|
||||
import {createRouter, createWebHistory} from 'vue-router'
|
||||
import {createApp} from 'vue'
|
||||
import ElementPlus from "element-plus"
|
||||
import "element-plus/dist/index.css"
|
||||
import App from './App.vue'
|
||||
import ChatPlus from "@/views/ChatPlus.vue";
|
||||
import Chat from "@/views/Chat.vue";
|
||||
import NotFound from './views/404.vue'
|
||||
import TestPage from './views/Test.vue'
|
||||
import Home from "@/views/Home.vue";
|
||||
import ChatFree from "@/views/ChatFree.vue";
|
||||
import Admin from "@/views/Admin.vue";
|
||||
import Login from "@/views/Login.vue"
|
||||
import Register from "@/views/Register.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -18,20 +18,20 @@ const routes = [
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plus', path: '/plus', component: ChatPlus, meta: {
|
||||
name: 'login', path: '/login', component: Login, meta: {
|
||||
title: '用户登录'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'register', path: '/register', component: Register, meta: {
|
||||
title: '用户注册'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'plus', path: '/chat', component: ChatPlus, meta: {
|
||||
title: 'ChatGPT-Plus for PC'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'mobile', path: '/mobile', component: Chat, meta: {
|
||||
title: 'ChatGPT-Plus for Mobile'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'free', path: '/free', component: ChatFree, meta: {
|
||||
title: 'Chat-Plus AI 助理'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'admin', path: '/admin', component: Admin, meta: {
|
||||
title: 'Chat-Plus 控制台'
|
||||
@@ -51,7 +51,7 @@ const routes = [
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
history: createWebHistory(),
|
||||
routes: routes,
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,15 @@ export function randString(length) {
|
||||
return buf.join("")
|
||||
}
|
||||
|
||||
export function UUID() {
|
||||
let d = new Date().getTime();
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = (d + Math.random() * 16) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
// 判断是否是移动设备
|
||||
export function isMobile() {
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
@@ -1,738 +0,0 @@
|
||||
<template>
|
||||
<div class="body" v-loading="loading">
|
||||
<div id="container">
|
||||
<div class="tool-box">
|
||||
<el-image style="width: 24px; height: 24px" :src="logo"/>
|
||||
<!-- <el-button round>WeChatGPT</el-button>-->
|
||||
<el-select v-model="role" class="chat-role"
|
||||
v-on:change="changeRole"
|
||||
placeholder="请选择对话角色">
|
||||
<el-option
|
||||
v-for="item in chatRoles"
|
||||
:key="item.key"
|
||||
:label="item.name"
|
||||
:value="item.key"
|
||||
>
|
||||
<div class="role-option">
|
||||
<el-image :src="item.icon"></el-image>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<el-button type="danger" class="clear-history" size="small" circle @click="clearChatHistory">
|
||||
<el-icon>
|
||||
<Delete/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
|
||||
<el-button type="info" size="small" class="config" ref="send-btn" circle @click="configDialog">
|
||||
<el-icon>
|
||||
<Tools/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="chat-box" id="chat-box" :style="{height: chatBoxHeight+'px'}">
|
||||
<div v-for="chat in chatData" :key="chat.id">
|
||||
<chat-prompt
|
||||
v-if="chat.type==='prompt'"
|
||||
:icon="chat.icon"
|
||||
:content="chat.content"/>
|
||||
<chat-reply v-else-if="chat.type==='reply'"
|
||||
:icon="chat.icon"
|
||||
:org-content="chat.orgContent"
|
||||
:content="chat.content"/>
|
||||
</div>
|
||||
|
||||
</div><!-- end chat box -->
|
||||
|
||||
<div class="input-box" :style="{width: inputBoxWidth+'px'}">
|
||||
<div class="re-generate">
|
||||
<div class="btn-box">
|
||||
<el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
|
||||
<el-icon>
|
||||
<VideoPause/>
|
||||
</el-icon>
|
||||
停止生成
|
||||
</el-button>
|
||||
|
||||
<el-button type="primary" v-if="showReGenerate" @click="reGenerate" plain>
|
||||
<el-icon>
|
||||
<RefreshRight/>
|
||||
</el-icon>
|
||||
重新生成
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-wrapper">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
ref="text-input"
|
||||
v-model="inputValue"
|
||||
:autosize="{ minRows: 1, maxRows: 10 }"
|
||||
v-on:keydown="inputKeyDown"
|
||||
v-on:focus="focus"
|
||||
autofocus
|
||||
type="textarea"
|
||||
placeholder="开始你的提问"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="btn-container">
|
||||
<el-row>
|
||||
<el-button type="success" class="send" :disabled="sending" v-on:click="sendMessage">发送</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- end input box -->
|
||||
|
||||
</div><!-- end container -->
|
||||
|
||||
<config-dialog v-model:show="showConfigDialog" :user="userInfo"></config-dialog>
|
||||
|
||||
<div class="token-dialog">
|
||||
<el-dialog
|
||||
v-model="showLoginDialog"
|
||||
:show-close="false"
|
||||
:close-on-click-modal="false"
|
||||
title="请输入口令继续访问"
|
||||
>
|
||||
<el-row>
|
||||
<el-input v-model="token" placeholder="在此输入口令" type="password" @keyup="loginInputKeyup">
|
||||
<template #prefix>
|
||||
<el-icon class="el-input__icon">
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="submitToken">提交</el-button>
|
||||
</el-row>
|
||||
|
||||
<div class="tip-text">
|
||||
打开微信扫下面二维码免费领取口令, <strong>强烈建议你使用 PC 浏览器访问获得更好的聊天体验。</strong>
|
||||
</div>
|
||||
|
||||
<el-row class="row-center">
|
||||
<el-image :src="sysConfig['wechat_card']" fit="cover" style="width: 250px;"/>
|
||||
</el-row>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent, nextTick} from 'vue'
|
||||
import ChatPrompt from "@/components/ChatPrompt.vue";
|
||||
import ChatReply from "@/components/ChatReply.vue";
|
||||
import {isMobile, randString, renderInputText} from "@/utils/libs";
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {Tools, Lock, Delete, VideoPause, RefreshRight} from '@element-plus/icons-vue'
|
||||
import ConfigDialog from '@/components/ConfigDialog.vue'
|
||||
import {httpPost, httpGet} from "@/utils/http";
|
||||
import {getSessionId, getUserInfo, setLoginUser} from "@/utils/storage";
|
||||
import hl from 'highlight.js'
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
import Clipboard from "clipboard";
|
||||
|
||||
export default defineComponent({
|
||||
name: "XChat",
|
||||
components: {RefreshRight, VideoPause, ChatPrompt, ChatReply, Tools, Lock, Delete, ConfigDialog},
|
||||
data() {
|
||||
return {
|
||||
logo: 'images/logo.png',
|
||||
chatData: [],
|
||||
chatRoles: [],
|
||||
role: 'gpt',
|
||||
inputValue: '', // 聊天内容
|
||||
chatBoxHeight: 0, // 聊天内容框高度
|
||||
|
||||
showConfigDialog: false,
|
||||
userInfo: {},
|
||||
showLoginDialog: false,
|
||||
sysConfig: {}, // 系统配置
|
||||
hasHelloMsg: {}, // 是否发送过打招呼信息
|
||||
|
||||
showStopGenerate: false,
|
||||
showReGenerate: false,
|
||||
canReGenerate: false, // 是否可以重新生
|
||||
previousText: '', // 上一次提问
|
||||
|
||||
token: '', // 会话 token
|
||||
replyIcon: 'images/avatar/gpt.png', // 回复信息的头像
|
||||
|
||||
lineBuffer: '', // 输出缓冲行
|
||||
connectingMessageBox: null, // 保存重连的消息框对象
|
||||
errorMessage: null, // 错误信息提示框
|
||||
socket: null,
|
||||
toolBoxHeight: 61 + 52, // 工具框的高度
|
||||
inputBoxWidth: window.innerWidth - 20,
|
||||
sending: true,
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function () {
|
||||
if (!isMobile()) {
|
||||
this.$router.push("plus");
|
||||
return;
|
||||
}
|
||||
|
||||
const clipboard = new Clipboard('.copy-reply');
|
||||
clipboard.on('success', () => {
|
||||
ElMessage.success('复制成功!');
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
ElMessage.error('复制失败!');
|
||||
})
|
||||
|
||||
nextTick(() => {
|
||||
this.chatBoxHeight = window.innerHeight - this.toolBoxHeight;
|
||||
ElMessage.warning("强烈建议使用PC浏览器访问获的更好的聊天体验!")
|
||||
})
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
this.chatBoxHeight = window.innerHeight - this.toolBoxHeight;
|
||||
this.inputBoxWidth = window.innerWidth - 20;
|
||||
});
|
||||
|
||||
// 获取系统配置
|
||||
httpGet('/api/config/get').then((res) => {
|
||||
this.sysConfig = res.data;
|
||||
}).catch(() => {
|
||||
ElMessage.error('获取系统配置失败')
|
||||
})
|
||||
|
||||
this.connect();
|
||||
},
|
||||
|
||||
methods: {
|
||||
configDialog: function () {
|
||||
this.showConfigDialog = true;
|
||||
this.userInfo = getUserInfo();
|
||||
},
|
||||
// 创建 socket 会话连接
|
||||
connect: function () {
|
||||
// 先关闭已有连接
|
||||
if (this.socket !== null) {
|
||||
this.activelyClose = true;
|
||||
this.socket.close();
|
||||
}
|
||||
|
||||
// 初始化 WebSocket 对象
|
||||
const sessionId = getSessionId();
|
||||
const socket = new WebSocket(process.env.VUE_APP_WS_HOST + `/api/chat?sessionId=${sessionId}&role=${this.role}`);
|
||||
socket.addEventListener('open', () => {
|
||||
// 获取聊天角色
|
||||
if (this.chatRoles.length === 0) {
|
||||
httpGet("/api/chat-roles/list").then((res) => {
|
||||
// ElMessage.success('创建会话成功!');
|
||||
this.chatRoles = res.data;
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
ElMessage.error("获取聊天角色失败");
|
||||
})
|
||||
} else {
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
this.sending = false; // 允许用户发送消息
|
||||
this.activelyClose = false;
|
||||
if (this.errorMessage !== null) {
|
||||
this.errorMessage.close(); // 关闭错误提示信息
|
||||
}
|
||||
// 加载聊天记录
|
||||
this.fetchChatHistory();
|
||||
});
|
||||
|
||||
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['is_hello_msg'] && this.hasHelloMsg[this.role]) { // 一定发送过打招呼信息的
|
||||
return
|
||||
}
|
||||
if (data.type === 'start') {
|
||||
this.chatData.push({
|
||||
type: "reply",
|
||||
id: randString(32),
|
||||
icon: this.replyIcon,
|
||||
content: "",
|
||||
});
|
||||
if (data['is_hello_msg'] !== true) {
|
||||
this.canReGenerate = true;
|
||||
}
|
||||
} else if (data.type === 'end') {
|
||||
this.sending = false;
|
||||
if (data['is_hello_msg'] !== true) {
|
||||
this.showReGenerate = true;
|
||||
} else {
|
||||
this.hasHelloMsg[this.role] = true
|
||||
}
|
||||
this.showStopGenerate = false;
|
||||
this.lineBuffer = ''; // 清空缓冲
|
||||
} else {
|
||||
this.lineBuffer += data.content;
|
||||
this.chatData[this.chatData.length - 1]['orgContent'] = this.lineBuffer;
|
||||
let md = require('markdown-it')();
|
||||
this.chatData[this.chatData.length - 1]['content'] = md.render(this.lineBuffer);
|
||||
|
||||
nextTick(() => {
|
||||
hl.configure({ignoreUnescapedHTML: true})
|
||||
const lines = document.querySelectorAll('.chat-line');
|
||||
const blocks = lines[lines.length - 1].querySelectorAll('pre code');
|
||||
blocks.forEach((block) => {
|
||||
hl.highlightElement(block)
|
||||
})
|
||||
})
|
||||
}
|
||||
// 将聊天框的滚动条滑动到最底部
|
||||
nextTick(() => {
|
||||
document.getElementById('container').scrollTo(0, document.getElementById('container').scrollHeight)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
if (this.activelyClose) { // 忽略主动关闭
|
||||
return;
|
||||
}
|
||||
|
||||
// 停止送消息
|
||||
this.sending = true;
|
||||
this.checkSession();
|
||||
});
|
||||
|
||||
this.socket = socket;
|
||||
},
|
||||
|
||||
checkSession: function () {
|
||||
// 检查会话
|
||||
httpGet("/api/session/get").then(() => {
|
||||
// 自动重新连接
|
||||
this.connect();
|
||||
}).catch((res) => {
|
||||
if (res.code === 400) {
|
||||
this.showLoginDialog = true;
|
||||
if (this.errorMessage !== null) {
|
||||
this.errorMessage.close();
|
||||
}
|
||||
} else {
|
||||
if (this.errorMessage === null) {
|
||||
this.errorMessage = ElMessage({
|
||||
message: '当前无法连接服务器,可检查网络设置是否正常',
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
showClose: false
|
||||
});
|
||||
}
|
||||
// 3 秒后继续重连
|
||||
setTimeout(() => this.checkSession(), 3000)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 更换角色
|
||||
changeRole: function () {
|
||||
this.loading = true
|
||||
// 清空对话列表
|
||||
this.chatData = [];
|
||||
this.hasHelloMsg = {};
|
||||
this.showStopGenerate = false;
|
||||
this.showReGenerate = false;
|
||||
this.connect();
|
||||
for (const key in this.chatRoles) {
|
||||
if (this.chatRoles[key].key === this.role) {
|
||||
this.replyIcon = this.chatRoles[key].icon;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 从后端获取聊天历史记录
|
||||
fetchChatHistory: function () {
|
||||
httpPost("/api/chat/history", {role: this.role}).then((res) => {
|
||||
if (this.chatData.length > 0) { // 如果已经有聊天记录了,就不追加了
|
||||
return
|
||||
}
|
||||
|
||||
const data = res.data
|
||||
const md = require('markdown-it')();
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].type === "prompt") {
|
||||
this.chatData.push(data[i]);
|
||||
continue;
|
||||
}
|
||||
data[i].orgContent = data[i].content;
|
||||
data[i].content = md.render(data[i].content);
|
||||
this.chatData.push(data[i]);
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
hl.configure({ignoreUnescapedHTML: true})
|
||||
const blocks = document.querySelector("#chat-box").querySelectorAll('pre code');
|
||||
blocks.forEach((block) => {
|
||||
hl.highlightElement(block)
|
||||
})
|
||||
})
|
||||
}).catch(() => {
|
||||
// console.error(e.message)
|
||||
})
|
||||
},
|
||||
|
||||
inputKeyDown: function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
if (this.sending) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
this.sendMessage();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 发送消息
|
||||
sendMessage: function (e) {
|
||||
// 强制按钮失去焦点
|
||||
if (e) {
|
||||
let target = e.target;
|
||||
if (target.nodeName === "SPAN") {
|
||||
target = e.target.parentNode;
|
||||
}
|
||||
target.blur();
|
||||
}
|
||||
|
||||
if (this.sending || this.inputValue.trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 追加消息
|
||||
this.chatData.push({
|
||||
type: "prompt",
|
||||
id: randString(32),
|
||||
icon: 'images/avatar/user.png',
|
||||
content: renderInputText(this.inputValue)
|
||||
});
|
||||
|
||||
this.sending = true;
|
||||
this.showStopGenerate = true;
|
||||
this.showReGenerate = false;
|
||||
this.socket.send(this.inputValue);
|
||||
this.$refs["text-input"].blur();
|
||||
this.previousText = this.inputValue;
|
||||
this.inputValue = '';
|
||||
// 等待 textarea 重新调整尺寸之后再自动获取焦点
|
||||
setTimeout(() => this.$refs["text-input"].focus(), 100);
|
||||
return true;
|
||||
},
|
||||
|
||||
// 获取焦点
|
||||
focus: function () {
|
||||
setTimeout(function () {
|
||||
document.getElementById('container').scrollTo(0, document.getElementById('container').scrollHeight)
|
||||
}, 200)
|
||||
},
|
||||
|
||||
// 提交 Token
|
||||
submitToken: function () {
|
||||
this.showLoginDialog = false;
|
||||
this.loading = true
|
||||
|
||||
// 获取会话
|
||||
httpPost("/api/login", {
|
||||
token: this.token
|
||||
}).then((res) => {
|
||||
setLoginUser(res.data)
|
||||
this.connect();
|
||||
this.loading = false;
|
||||
}).catch(() => {
|
||||
ElMessage.error("口令错误");
|
||||
this.token = '';
|
||||
this.showLoginDialog = true;
|
||||
this.loading = false;
|
||||
})
|
||||
},
|
||||
|
||||
// 登录输入框输入事件处理
|
||||
loginInputKeyup: function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
this.submitToken();
|
||||
}
|
||||
},
|
||||
|
||||
// 清空聊天记录
|
||||
clearChatHistory: function () {
|
||||
ElMessageBox.confirm(
|
||||
'确认要清空当前角色聊天历史记录吗?<br/>此操作不可以撤销!',
|
||||
'操作提示:',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
dangerouslyUseHTMLString: true,
|
||||
showClose: true,
|
||||
closeOnClickModal: false,
|
||||
center: true,
|
||||
}
|
||||
).then(() => {
|
||||
httpPost("/api/chat/history/clear", {role: this.role}).then(() => {
|
||||
ElMessage.success("当前角色会话已清空");
|
||||
this.chatData = [];
|
||||
}).catch(() => {
|
||||
ElMessage.error("删除失败")
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
},
|
||||
|
||||
// 停止生成
|
||||
stopGenerate: function () {
|
||||
this.showStopGenerate = false;
|
||||
httpPost("/api/chat/stop").then(() => {
|
||||
console.log("stopped generate.")
|
||||
this.sending = false;
|
||||
if (this.canReGenerate) {
|
||||
this.showReGenerate = true;
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 重新生成
|
||||
reGenerate: function () {
|
||||
this.sending = true;
|
||||
this.showStopGenerate = true;
|
||||
this.showReGenerate = false;
|
||||
this.socket.send('重新生成上述问题的答案:' + this.previousText);
|
||||
}
|
||||
},
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
#app {
|
||||
height: 100%;
|
||||
|
||||
.body {
|
||||
background-color: rgba(247, 247, 248, 1);
|
||||
|
||||
background-image url("~@/assets/img/bg_01.jpeg")
|
||||
display flex;
|
||||
//justify-content center;
|
||||
align-items flex-start;
|
||||
height 100%;
|
||||
|
||||
#container {
|
||||
overflow auto;
|
||||
width 100%;
|
||||
|
||||
.tool-box {
|
||||
padding-top 10px;
|
||||
display flex;
|
||||
justify-content center;
|
||||
align-items center;
|
||||
|
||||
.el-select {
|
||||
max-width 120px;
|
||||
}
|
||||
|
||||
.chat-role {
|
||||
margin-left 5px;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
margin-right 5px;
|
||||
}
|
||||
|
||||
.clear-history, .config {
|
||||
margin-left 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-box {
|
||||
// 变量定义
|
||||
--content-font-size: 16px;
|
||||
--content-color: #374151;
|
||||
|
||||
font-family 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
padding: 0 10px 10px 10px;
|
||||
|
||||
.chat-line {
|
||||
padding 10px 5px;
|
||||
font-size 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
.chat-icon {
|
||||
img {
|
||||
width 32px;
|
||||
height 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.input-box {
|
||||
padding 10px;
|
||||
background #ffffff;
|
||||
position: absolute;
|
||||
bottom: 0
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-flow: column;
|
||||
|
||||
.re-generate {
|
||||
position relative
|
||||
display flex
|
||||
justify-content center
|
||||
|
||||
.btn-box {
|
||||
position absolute
|
||||
bottom 20px
|
||||
|
||||
.el-icon {
|
||||
margin-right 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
width 100%;
|
||||
display flex;
|
||||
|
||||
.input-container {
|
||||
overflow hidden
|
||||
width 100%
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
padding: 5px 10px;
|
||||
|
||||
.el-textarea__inner {
|
||||
box-shadow: none
|
||||
padding 5px 0
|
||||
}
|
||||
|
||||
.el-textarea__inner::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
margin-left 10px;
|
||||
|
||||
.el-row {
|
||||
flex-wrap nowrap
|
||||
//width 106px;
|
||||
align-items center
|
||||
}
|
||||
|
||||
.send {
|
||||
width 60px;
|
||||
height 40px;
|
||||
background-color: var(--el-color-success)
|
||||
}
|
||||
|
||||
.is-disabled {
|
||||
background-color: var(--el-button-disabled-bg-color);
|
||||
border-color: var(--el-button-disabled-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// end of input wrapper
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#container::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.row-center {
|
||||
justify-content center
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
width 90%;
|
||||
max-width 420px;
|
||||
}
|
||||
|
||||
.el-message {
|
||||
min-width: 100px;
|
||||
max-width 600px;
|
||||
}
|
||||
|
||||
.token-dialog {
|
||||
.el-dialog {
|
||||
--el-dialog-width 90%;
|
||||
max-width 400px;
|
||||
|
||||
.el-dialog__body {
|
||||
padding 10px 10px 20px 10px;
|
||||
|
||||
.el-row {
|
||||
flex-wrap nowrap
|
||||
|
||||
button {
|
||||
margin-left 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
text-align left
|
||||
padding 20px;
|
||||
|
||||
.el-alert {
|
||||
padding 5px;
|
||||
|
||||
.el-alert__description {
|
||||
font-size 14px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.el-select-dropdown {
|
||||
.el-select-dropdown__item {
|
||||
padding 8px 5px;
|
||||
|
||||
.role-option {
|
||||
display flex
|
||||
flex-flow row
|
||||
|
||||
.el-image {
|
||||
width 20px
|
||||
height 20px
|
||||
border-radius 50%
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left 5px;
|
||||
height 20px;
|
||||
line-height 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,25 +2,20 @@
|
||||
<div>{{ title }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue"
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import {isMobile} from "@/utils/libs";
|
||||
import {useRouter} from "vue-router"; // 导入useRouter函数
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HomePage',
|
||||
data () {
|
||||
return {
|
||||
title: "Loading page...",
|
||||
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (isMobile()) {
|
||||
this.$router.push("mobile");
|
||||
} else {
|
||||
this.$router.push("plus");
|
||||
}
|
||||
|
||||
const title = ref("Loading page...");
|
||||
const router = useRouter();
|
||||
onMounted(() => {
|
||||
if (isMobile()) {
|
||||
router.push("mobile");
|
||||
} else {
|
||||
router.push("chat");
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
155
web/src/views/Login.vue
Normal file
155
web/src/views/Login.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg"></div>
|
||||
<div class="main">
|
||||
<div class="contain">
|
||||
<div class="header">{{ title }}</div>
|
||||
<div class="content">
|
||||
<div class="block">
|
||||
<el-input placeholder="手机号/邮箱" size="large" v-model="username" autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="请输入密码" size="large" v-model="password" show-password autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-row class="btn-row">
|
||||
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-row class="text-line">
|
||||
还没有账号?
|
||||
<el-link type="primary" @click="router.push('register')">注册新账号</el-link>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {Lock, UserFilled} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {setLoginUser} from "@/utils/storage";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref('ChatGPT Plus 用户登录');
|
||||
const username = ref('geekmaster');
|
||||
const password = ref('12345678');
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
login();
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const login = function () {
|
||||
if (username.value === '') {
|
||||
return ElMessage.error('请输入用户名');
|
||||
}
|
||||
if (password.value.trim() === '') {
|
||||
return ElMessage.error('请输入密码');
|
||||
}
|
||||
|
||||
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
|
||||
setLoginUser(res.data)
|
||||
router.push("chat")
|
||||
|
||||
}).catch((e) => {
|
||||
ElMessage.error('登录失败,' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.bg {
|
||||
position fixed
|
||||
left 0
|
||||
right 0
|
||||
top 0
|
||||
bottom 0
|
||||
background-color #313237
|
||||
background-image url("~@/assets/img/login-bg.png")
|
||||
background-size cover
|
||||
background-position center
|
||||
background-repeat repeat-y
|
||||
//filter: blur(10px); /* 调整模糊程度,可以根据需要修改值 */
|
||||
}
|
||||
|
||||
.main {
|
||||
.contain {
|
||||
position fixed
|
||||
left 50%
|
||||
top 50%
|
||||
width 90%
|
||||
max-width 400px;
|
||||
transform translate(-50%, -50%)
|
||||
padding 20px 10px;
|
||||
color #ffffff
|
||||
border-radius 10px;
|
||||
|
||||
.header {
|
||||
width 100%
|
||||
margin-bottom 24px
|
||||
font-size 24px
|
||||
color $white_v1
|
||||
letter-space 2px
|
||||
text-align center
|
||||
}
|
||||
|
||||
.content {
|
||||
width 100%
|
||||
height: auto
|
||||
border-radius 3px
|
||||
|
||||
.block {
|
||||
margin-bottom 16px
|
||||
|
||||
.el-input__inner {
|
||||
border 1px solid $gray-v6 !important
|
||||
|
||||
.el-icon-user, .el-icon-lock {
|
||||
font-size 20px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
padding-top 10px;
|
||||
|
||||
.login-btn {
|
||||
width 100%
|
||||
font-size 16px
|
||||
letter-spacing 2px
|
||||
}
|
||||
}
|
||||
|
||||
.text-line {
|
||||
justify-content center
|
||||
padding-top 10px;
|
||||
font-size 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
171
web/src/views/Register.vue
Normal file
171
web/src/views/Register.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="bg"></div>
|
||||
<div class="main">
|
||||
<div class="contain">
|
||||
<div class="header">{{ title }}</div>
|
||||
<div class="content">
|
||||
<el-form :model="formData" label-width="120px" ref="formRef" :rules="rules">
|
||||
<div class="block">
|
||||
<el-input placeholder="手机号/邮箱(4-30位)"
|
||||
size="large" maxlength="30"
|
||||
v-model="formData.username"
|
||||
autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="请输入密码(8-16位)"
|
||||
maxlength="16" size="large"
|
||||
v-model="formData.password" show-password
|
||||
autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<el-input placeholder="重复密码(8-16位)"
|
||||
size="large" maxlength="16" v-model="formData.repass" show-password
|
||||
autocomplete="off">
|
||||
<template #prefix>
|
||||
<el-icon>
|
||||
<Lock/>
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-row class="btn-row">
|
||||
<el-button class="login-btn" size="large" type="primary" @click="register">注册</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-row class="text-line">
|
||||
已经有账号?
|
||||
<el-link type="primary" @click="router.push('login')">登录</el-link>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {ref} from "vue";
|
||||
import {Lock, UserFilled} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref('ChatGPT Plus 用户注册');
|
||||
const formData = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
repass: '',
|
||||
})
|
||||
const formRef = ref(null)
|
||||
|
||||
const register = function () {
|
||||
if (formData.value.username.length < 4) {
|
||||
return ElMessage.error('用户名的长度为4-30个字符');
|
||||
}
|
||||
if (formData.value.password.length < 8) {
|
||||
return ElMessage.error('密码的长度为8-16个字符');
|
||||
}
|
||||
if (formData.value.repass !== formData.value.password) {
|
||||
return ElMessage.error('两次输入密码不一致');
|
||||
}
|
||||
|
||||
httpPost('/api/user/register', formData.value).then(() => {
|
||||
ElMessage.success({"message": "注册成功,即将跳转到登录页...", onClose: () => router.push("login")})
|
||||
}).catch((e) => {
|
||||
ElMessage.error('注册失败,' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.bg {
|
||||
position fixed
|
||||
left 0
|
||||
right 0
|
||||
top 0
|
||||
bottom 0
|
||||
background-color #091519
|
||||
background-image url("~@/assets/img/reg-bg.png")
|
||||
background-size cover
|
||||
background-position center
|
||||
background-repeat no-repeat
|
||||
//filter: blur(10px); /* 调整模糊程度,可以根据需要修改值 */
|
||||
}
|
||||
|
||||
.main {
|
||||
.contain {
|
||||
position fixed
|
||||
left 50%
|
||||
top 40%
|
||||
width 90%
|
||||
max-width 400px
|
||||
transform translate(-50%, -50%)
|
||||
padding 20px;
|
||||
color #ffffff
|
||||
border-radius 10px;
|
||||
background rgba(255, 255, 255, 0.3)
|
||||
|
||||
.header {
|
||||
width 100%
|
||||
margin-bottom 24px
|
||||
font-size 24px
|
||||
color $white_v1
|
||||
letter-space 2px
|
||||
text-align center
|
||||
}
|
||||
|
||||
.content {
|
||||
width 100%
|
||||
height: auto
|
||||
border-radius 3px
|
||||
|
||||
.block {
|
||||
margin-bottom 16px
|
||||
|
||||
.el-input__inner {
|
||||
border 1px solid $gray-v6 !important
|
||||
|
||||
.el-icon-user, .el-icon-lock {
|
||||
font-size 20px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
padding-top 10px;
|
||||
|
||||
.login-btn {
|
||||
width 100%
|
||||
font-size 16px
|
||||
letter-spacing 2px
|
||||
}
|
||||
}
|
||||
|
||||
.text-line {
|
||||
justify-content center
|
||||
padding-top 10px;
|
||||
font-size 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,49 +1,165 @@
|
||||
<template>
|
||||
<div style="padding: 20px;" v-html="content" id="content"></div>
|
||||
<div class="role-list">
|
||||
|
||||
|
||||
<el-form :model="form1" label-width="120px" ref="formRef" :rules="rules">
|
||||
<el-form-item label="角色名称:" prop="name">
|
||||
<el-input
|
||||
v-model="form1.name"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色标志:" prop="key">
|
||||
<el-input
|
||||
v-model="form1.key"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="角色图标:" prop="icon">
|
||||
<el-input
|
||||
v-model="form1.icon"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="打招呼信息:" prop="hello_msg">
|
||||
<el-input
|
||||
v-model="form1.hello_msg"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="上下文信息:" prop="context">
|
||||
<template #default>
|
||||
<el-table :data="form1.context" :border="childBorder" size="small">
|
||||
<el-table-column label="对话角色" width="120">
|
||||
<template #default="scope">
|
||||
<el-input
|
||||
v-model="scope.row.role"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="对话内容">
|
||||
<template #header>
|
||||
<div class="context-msg-key">
|
||||
<span>对话内容</span>
|
||||
<span class="fr">
|
||||
<el-button type="primary" @click="addContext" size="small">
|
||||
<el-icon>
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
增加一行
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="scope">
|
||||
<div class="context-msg-content">
|
||||
<el-input
|
||||
v-model="scope.row.content"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<span><el-icon @click="removeContext(scope.$index)"><RemoveFilled/></el-icon></span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="form1.enable"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="doUpdate">保存</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {defineComponent, nextTick} from "vue"
|
||||
import hl from 'highlight.js'
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
<script setup>
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TestPage',
|
||||
data() {
|
||||
return {
|
||||
content: "测试页面",
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
import {Plus, RemoveFilled} from "@element-plus/icons-vue";
|
||||
import {reactive, ref} from "vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
let md = require('markdown-it')();
|
||||
this.content = md.render("```\n" +
|
||||
"const socket = new WebSocket('ws://localhost:8080');\n" +
|
||||
"\n" +
|
||||
"// 连接成功\n" +
|
||||
"socket.addEventListener('open', event => {\n" +
|
||||
" console.log('WebSocket 连接成功!');\n" +
|
||||
"});\n" +
|
||||
"\n" +
|
||||
"// 接收消息\n" +
|
||||
"socket.addEventListener('message', event => {\n" +
|
||||
" console.log('收到消息:', event.data);\n" +
|
||||
"});\n" +
|
||||
"\n" +
|
||||
"// 发送消息\n" +
|
||||
"socket.send('Hello, WebSocket!');\n" +
|
||||
"\n" +
|
||||
"```\n" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"以上代码创建了一个 WebSocket 连接,并在连接成功后输出一条提示信息。当收到消息时,会在控制台打印该消息。同时还演示了如何发送消息。在实际应用中,不同的框架和库可能会提供不同的 WebSocket 实现,代码可能会有所区别。");
|
||||
nextTick(() => {
|
||||
const blocks = document.getElementById('content').querySelectorAll('pre code');
|
||||
console.log(blocks)
|
||||
blocks.forEach((block) => {
|
||||
hl.highlightBlock(block)
|
||||
})
|
||||
})
|
||||
}
|
||||
const showDialog = ref(false)
|
||||
const childBorder = ref(true)
|
||||
const form1 = ref({context: []})
|
||||
// const form2 = ref({context: []})
|
||||
const formRef = ref(null)
|
||||
|
||||
const rules = reactive({
|
||||
name: [{required: true, message: '请输入用户名', trigger: 'change',}],
|
||||
key: [{required: true, message: '请输入角色标识', trigger: 'change',}],
|
||||
icon: [{required: true, message: '请输入角色图标', trigger: 'change',}],
|
||||
hello_msg: [{required: true, message: '请输入打招呼信息', trigger: 'change',}]
|
||||
})
|
||||
|
||||
|
||||
// 编辑
|
||||
const doUpdate = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
showDialog.value = false
|
||||
httpPost('/api/admin/chat-roles/set', form1.value).then(() => {
|
||||
ElMessage.success('更新角色成功')
|
||||
// 更新当前数据行
|
||||
}).catch((e) => {
|
||||
ElMessage.error('更新角色失败,' + e.message)
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const addContext = function () {
|
||||
form1.value.context.push({role: '', content: ''})
|
||||
}
|
||||
|
||||
const removeContext = function (index) {
|
||||
form1.value.context.splice(index, 1);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.role-list {
|
||||
.opt-box {
|
||||
padding-bottom: 10px;
|
||||
|
||||
.el-icon {
|
||||
margin-right 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.context-msg-key {
|
||||
.fr {
|
||||
float right
|
||||
|
||||
.el-icon {
|
||||
margin-right 5px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-msg-content {
|
||||
display flex
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
margin-top 5px;
|
||||
margin-left 5px;
|
||||
cursor pointer
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -416,12 +416,12 @@ const batchAddUser = function () {
|
||||
padding-bottom: 10px;
|
||||
|
||||
.el-icon {
|
||||
margin-right 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width 100%
|
||||
width: 100%
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user