refactor: V3 版本重构已基本完成

This commit is contained in:
RockYang
2023-06-15 09:41:30 +08:00
parent be6e8c7713
commit 567cdc6f8d
99 changed files with 5209 additions and 5752 deletions

View File

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

View File

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

View File

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

View File

@@ -416,12 +416,12 @@ const batchAddUser = function () {
padding-bottom: 10px;
.el-icon {
margin-right 5px;
margin-right: 5px;
}
}
.el-select {
width 100%
width: 100%
}
}