mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-11-13 04:33:42 +08:00
feat:chat style
This commit is contained in:
@@ -56,5 +56,5 @@ import FooterBar from "@/components/FooterBar.vue";
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
<template>
|
||||
<button v-if="showButton" @click="scrollToTop" class="scroll-to-top" :style="{bottom: bottom + 'px', right: right + 'px', backgroundColor: bgColor}">
|
||||
<button
|
||||
v-if="showButton"
|
||||
@click="scrollToTop"
|
||||
class="scroll-to-top"
|
||||
:style="{
|
||||
bottom: bottom + 'px',
|
||||
right: right + 'px',
|
||||
backgroundColor: bgColor
|
||||
}"
|
||||
>
|
||||
<el-icon><ArrowUpBold /></el-icon>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ArrowUpBold} from "@element-plus/icons-vue";
|
||||
import { ArrowUpBold } from "@element-plus/icons-vue";
|
||||
|
||||
export default {
|
||||
name: 'BackTop',
|
||||
components: {ArrowUpBold},
|
||||
name: "BackTop",
|
||||
components: { ArrowUpBold },
|
||||
props: {
|
||||
bottom: {
|
||||
type: Number,
|
||||
default: 30
|
||||
default: 155
|
||||
},
|
||||
right: {
|
||||
type: Number,
|
||||
@@ -21,7 +30,7 @@ export default {
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: '#007bff'
|
||||
default: "#b6aaf9"
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -31,19 +40,19 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.checkScroll();
|
||||
window.addEventListener('resize', this.checkScroll);
|
||||
this.$el.parentElement.addEventListener('scroll', this.checkScroll);
|
||||
window.addEventListener("resize", this.checkScroll);
|
||||
this.$el.parentElement.addEventListener("scroll", this.checkScroll);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('resize', this.checkScroll);
|
||||
this.$el.parentElement.removeEventListener('scroll', this.checkScroll);
|
||||
window.removeEventListener("resize", this.checkScroll);
|
||||
this.$el.parentElement.removeEventListener("scroll", this.checkScroll);
|
||||
},
|
||||
methods: {
|
||||
scrollToTop() {
|
||||
const container = this.$el.parentElement;
|
||||
container.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
behavior: "smooth"
|
||||
});
|
||||
},
|
||||
checkScroll() {
|
||||
@@ -51,7 +60,7 @@ export default {
|
||||
this.showButton = container.scrollTop > 50;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@@ -63,15 +72,15 @@ export default {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: opacity 0.3s;
|
||||
width 40px
|
||||
height 40px
|
||||
width 30px
|
||||
height 30px
|
||||
display flex
|
||||
justify-content center
|
||||
align-items center
|
||||
font-size 20px
|
||||
font-size 18px
|
||||
|
||||
&:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,65 +1,77 @@
|
||||
<template>
|
||||
<div class="chat-line chat-line-prompt-list" v-if="listStyle === 'list'">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="data.icon" alt="User"/>
|
||||
</div>
|
||||
<div class="chat-icon">
|
||||
<img :src="data.icon" alt="User" />
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div v-if="files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover"/>
|
||||
<div class="chat-item">
|
||||
<div v-if="files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
<div class="body">
|
||||
<div class="title">
|
||||
<el-link
|
||||
:href="file.url"
|
||||
target="_blank"
|
||||
style="--el-font-weight-primary: bold"
|
||||
>{{ file.name }}</el-link
|
||||
>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title">
|
||||
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span>{{GetFileType(file.ext)}}</span>
|
||||
<span>{{FormatFileSize(file.size)}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span>{{ GetFileType(file.ext) }}</span>
|
||||
<span>{{ FormatFileSize(file.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
|
||||
<span class="bar-item">tokens: {{ finalTokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" v-html="content"></div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"
|
||||
><el-icon><Clock /></el-icon>
|
||||
{{ dateFormat(data.created_at) }}</span
|
||||
>
|
||||
<span class="bar-item">tokens: {{ finalTokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-line chat-line-prompt-chat" v-else>
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="data.icon" alt="User"/>
|
||||
<img :src="data.icon" alt="User" />
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
|
||||
<div v-if="files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover"/>
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title">
|
||||
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{file.name}}</el-link>
|
||||
<el-link
|
||||
:href="file.url"
|
||||
target="_blank"
|
||||
style="--el-font-weight-primary: bold"
|
||||
>{{ file.name }}</el-link
|
||||
>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span>{{GetFileType(file.ext)}}</span>
|
||||
<span>{{FormatFileSize(file.size)}}</span>
|
||||
<span>{{ GetFileType(file.ext) }}</span>
|
||||
<span>{{ FormatFileSize(file.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,8 +81,11 @@
|
||||
<div class="content" v-html="content"></div>
|
||||
</div>
|
||||
<div class="bar" v-if="data.created_at > 0">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
|
||||
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
|
||||
<span class="bar-item"
|
||||
><el-icon><Clock /></el-icon>
|
||||
{{ dateFormat(data.created_at) }}</span
|
||||
>
|
||||
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,104 +93,110 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue"
|
||||
import {Clock} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { Clock } from "@element-plus/icons-vue";
|
||||
import { httpPost } from "@/utils/http";
|
||||
import hl from "highlight.js";
|
||||
import {dateFormat, isImage, processPrompt} from "@/utils/libs";
|
||||
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
|
||||
import { dateFormat, isImage, processPrompt } from "@/utils/libs";
|
||||
import { FormatFileSize, GetFileIcon, GetFileType } from "@/store/system";
|
||||
|
||||
const mathjaxPlugin = require('markdown-it-mathjax3')
|
||||
const md = require('markdown-it')({
|
||||
const mathjaxPlugin = require("markdown-it-mathjax3");
|
||||
const md = require("markdown-it")({
|
||||
breaks: true,
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
highlight: function (str, lang) {
|
||||
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
|
||||
const codeIndex =
|
||||
parseInt(Date.now()) + Math.floor(Math.random() * 10000000);
|
||||
// 显示复制代码按钮
|
||||
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
|
||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '</textarea>')}</textarea>`
|
||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
|
||||
/<\/textarea>/g,
|
||||
"</textarea>"
|
||||
)}</textarea>`;
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
const langHtml = `<span class="lang-name">${lang}</span>`
|
||||
const langHtml = `<span class="lang-name">${lang}</span>`;
|
||||
// 处理代码高亮
|
||||
const preCode = hl.highlight(lang, str, true).value
|
||||
const preCode = hl.highlight(lang, str, true).value;
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`;
|
||||
}
|
||||
|
||||
// 处理代码高亮
|
||||
const preCode = md.utils.escapeHtml(str)
|
||||
const preCode = md.utils.escapeHtml(str);
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`;
|
||||
}
|
||||
});
|
||||
md.use(mathjaxPlugin)
|
||||
md.use(mathjaxPlugin);
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: {
|
||||
content: '',
|
||||
created_at: '',
|
||||
content: "",
|
||||
created_at: "",
|
||||
tokens: 0,
|
||||
model: '',
|
||||
icon: '',
|
||||
},
|
||||
model: "",
|
||||
icon: ""
|
||||
}
|
||||
},
|
||||
listStyle: {
|
||||
type: String,
|
||||
default: 'list',
|
||||
},
|
||||
})
|
||||
const finalTokens = ref(props.data.tokens)
|
||||
const content =ref(processPrompt(props.data.content))
|
||||
const files = ref([])
|
||||
default: "list"
|
||||
}
|
||||
});
|
||||
const finalTokens = ref(props.data.tokens);
|
||||
const content = ref(processPrompt(props.data.content));
|
||||
const files = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
processFiles()
|
||||
})
|
||||
processFiles();
|
||||
});
|
||||
|
||||
const processFiles = () => {
|
||||
if (!props.data.content) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const linkRegex = /(https?:\/\/\S+)/g;
|
||||
const links = props.data.content.match(linkRegex);
|
||||
if (links) {
|
||||
httpPost("/api/upload/list", {urls: links}).then(res => {
|
||||
files.value = res.data.items
|
||||
httpPost("/api/upload/list", { urls: links })
|
||||
.then((res) => {
|
||||
files.value = res.data.items;
|
||||
|
||||
for (let link of links) {
|
||||
if (isExternalImg(link, files.value)) {
|
||||
files.value.push({url:link, ext: ".png"})
|
||||
for (let link of links) {
|
||||
if (isExternalImg(link, files.value)) {
|
||||
files.value.push({ url: link, ext: ".png" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
for (let link of links) {
|
||||
content.value = content.value.replace(link,"")
|
||||
content.value = content.value.replace(link, "");
|
||||
}
|
||||
|
||||
}
|
||||
content.value = md.render(content.value.trim())
|
||||
}
|
||||
content.value = md.render(content.value.trim());
|
||||
};
|
||||
const isExternalImg = (link, files) => {
|
||||
return isImage(link) && !files.find(file => file.url === link)
|
||||
}
|
||||
return isImage(link) && !files.find((file) => file.url === link);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import '@/assets/css/markdown/vue.css';
|
||||
.chat-page,.chat-export {
|
||||
.chat-line-prompt-list {
|
||||
background-color #ffffff;
|
||||
|
||||
background-color:var( --chat-content-bg-list);
|
||||
color:var(--theme-text-color-primary);
|
||||
justify-content: center;
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
border-bottom: 0.5px solid var(--el-border-color);
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
@@ -189,7 +210,7 @@ const isExternalImg = (link, files) => {
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
border-radius: 50%;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
@@ -218,8 +239,9 @@ const isExternalImg = (link, files) => {
|
||||
display flex
|
||||
flex-flow row
|
||||
border-radius 10px
|
||||
background-color #ffffff
|
||||
background-color:var(--chat-content-bg);
|
||||
border 1px solid #e3e3e3
|
||||
color:var(--theme-text-color-primary);
|
||||
padding 6px
|
||||
margin-bottom 10px
|
||||
|
||||
@@ -251,7 +273,7 @@ const isExternalImg = (link, files) => {
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 0;
|
||||
color #374151;
|
||||
color:var(--theme-text-color-primary);
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
@@ -279,7 +301,7 @@ const isExternalImg = (link, files) => {
|
||||
padding 10px 10px 10px 0;
|
||||
|
||||
.bar-item {
|
||||
background-color #f7f7f8;
|
||||
// background-color #f7f7f8;
|
||||
color #888
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
@@ -298,7 +320,7 @@ const isExternalImg = (link, files) => {
|
||||
}
|
||||
|
||||
.chat-line-prompt-chat {
|
||||
background-color #ffffff;
|
||||
background: var(--chat-bg);
|
||||
justify-content: center;
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
@@ -345,7 +367,8 @@ const isExternalImg = (link, files) => {
|
||||
display flex
|
||||
flex-flow row
|
||||
border-radius 10px
|
||||
background-color #ffffff
|
||||
background-color:var(--chat-content-bg);
|
||||
color:var(--theme-text-color-primary);
|
||||
border 1px solid #e3e3e3
|
||||
padding 6px
|
||||
margin-bottom 10px
|
||||
@@ -382,10 +405,10 @@ const isExternalImg = (link, files) => {
|
||||
.content {
|
||||
word-break break-word;
|
||||
padding: 1rem
|
||||
color #222222;
|
||||
color var(--theme-text-primary);
|
||||
font-size: var(--content-font-size);
|
||||
overflow: auto;
|
||||
background-color #98e165
|
||||
background-color :var(--chat-content-bg);
|
||||
border-radius: 10px 0 10px 10px;
|
||||
|
||||
img {
|
||||
|
||||
@@ -2,48 +2,54 @@
|
||||
<div class="chat-line chat-line-reply-list" v-if="listStyle === 'list'">
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="data.icon" alt="ChatGPT">
|
||||
<img :src="data.icon" alt="ChatGPT" />
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="content" v-html="md.render(processContent(data.content))"></div>
|
||||
<div
|
||||
class="content"
|
||||
v-html="md.render(processContent(data.content))"
|
||||
></div>
|
||||
<div class="bar" v-if="data.created_at">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
|
||||
<span class="bar-item">tokens: {{ data.tokens }}</span>
|
||||
<span class="bar-item"
|
||||
><el-icon><Clock /></el-icon>
|
||||
{{ dateFormat(data.created_at) }}</span
|
||||
>
|
||||
<span class="bar-item">tokens: {{ data.tokens }}</span>
|
||||
<span class="bar-item">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="复制回答"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon class="copy-reply" :data-clipboard-text="data.content">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="复制回答"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon class="copy-reply" :data-clipboard-text="data.content">
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span v-if="!readOnly">
|
||||
<span class="bar-item" @click="reGenerate(data.prompt)">
|
||||
<el-tooltip
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="重新生成"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon><Refresh/></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
>
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<span class="bar-item" @click="synthesis(data.content)">
|
||||
<el-tooltip
|
||||
<span class="bar-item" @click="synthesis(data.content)">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="生成语音朗读"
|
||||
placement="bottom"
|
||||
>
|
||||
<i class="iconfont icon-speaker"></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
>
|
||||
<i class="iconfont icon-speaker"></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
<!-- <span class="bar-item">-->
|
||||
<!-- <el-dropdown trigger="click">-->
|
||||
@@ -65,49 +71,55 @@
|
||||
<div class="chat-line chat-line-reply-chat" v-else>
|
||||
<div class="chat-line-inner">
|
||||
<div class="chat-icon">
|
||||
<img :src="data.icon" alt="ChatGPT">
|
||||
<img :src="data.icon" alt="ChatGPT" />
|
||||
</div>
|
||||
<div class="chat-item">
|
||||
<div class="content-wrapper">
|
||||
<div class="content" v-html="md.render(processContent(data.content))"></div>
|
||||
<div
|
||||
class="content"
|
||||
v-html="md.render(processContent(data.content))"
|
||||
></div>
|
||||
</div>
|
||||
<div class="bar" v-if="data.created_at">
|
||||
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
|
||||
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
|
||||
<span class="bar-item"
|
||||
><el-icon><Clock /></el-icon>
|
||||
{{ dateFormat(data.created_at) }}</span
|
||||
>
|
||||
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
|
||||
<span class="bar-item bg">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="复制回答"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon class="copy-reply" :data-clipboard-text="data.content">
|
||||
<DocumentCopy/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="复制回答"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon class="copy-reply" :data-clipboard-text="data.content">
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span v-if="!readOnly">
|
||||
<span class="bar-item bg" @click="reGenerate(data.prompt)">
|
||||
<el-tooltip
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="重新生成"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon><Refresh/></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
>
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<span class="bar-item bg" @click="synthesis(data.content)">
|
||||
<el-tooltip
|
||||
<span class="bar-item bg" @click="synthesis(data.content)">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="生成语音朗读"
|
||||
placement="bottom"
|
||||
>
|
||||
<i class="iconfont icon-speaker"></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
>
|
||||
<i class="iconfont icon-speaker"></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,9 +128,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {Clock, DocumentCopy, Refresh} from "@element-plus/icons-vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {dateFormat, processContent} from "@/utils/libs";
|
||||
import { Clock, DocumentCopy, Refresh } from "@element-plus/icons-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { dateFormat, processContent } from "@/utils/libs";
|
||||
import hl from "highlight.js";
|
||||
// eslint-disable-next-line no-undef,no-unused-vars
|
||||
const props = defineProps({
|
||||
@@ -128,8 +140,8 @@ const props = defineProps({
|
||||
icon: "",
|
||||
content: "",
|
||||
created_at: "",
|
||||
tokens: 0,
|
||||
},
|
||||
tokens: 0
|
||||
}
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
@@ -137,53 +149,57 @@ const props = defineProps({
|
||||
},
|
||||
listStyle: {
|
||||
type: String,
|
||||
default: 'list',
|
||||
},
|
||||
})
|
||||
default: "list"
|
||||
}
|
||||
});
|
||||
|
||||
const mathjaxPlugin = require('markdown-it-mathjax3')
|
||||
const md = require('markdown-it')({
|
||||
const mathjaxPlugin = require("markdown-it-mathjax3");
|
||||
const md = require("markdown-it")({
|
||||
breaks: true,
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
highlight: function (str, lang) {
|
||||
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000)
|
||||
const codeIndex =
|
||||
parseInt(Date.now()) + Math.floor(Math.random() * 10000000);
|
||||
// 显示复制代码按钮
|
||||
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
|
||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(/<\/textarea>/g, '</textarea>')}</textarea>`
|
||||
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
|
||||
/<\/textarea>/g,
|
||||
"</textarea>"
|
||||
)}</textarea>`;
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
const langHtml = `<span class="lang-name">${lang}</span>`
|
||||
const langHtml = `<span class="lang-name">${lang}</span>`;
|
||||
// 处理代码高亮
|
||||
const preCode = hl.highlight(lang, str, true).value
|
||||
const preCode = hl.highlight(lang, str, true).value;
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn} ${langHtml}</pre>`;
|
||||
}
|
||||
|
||||
// 处理代码高亮
|
||||
const preCode = md.utils.escapeHtml(str)
|
||||
const preCode = md.utils.escapeHtml(str);
|
||||
// 将代码包裹在 pre 中
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`;
|
||||
}
|
||||
});
|
||||
md.use(mathjaxPlugin)
|
||||
md.use(mathjaxPlugin);
|
||||
|
||||
const emits = defineEmits(['regen']);
|
||||
const emits = defineEmits(["regen"]);
|
||||
|
||||
if (!props.data.icon) {
|
||||
props.data.icon = "images/gpt-icon.png"
|
||||
props.data.icon = "images/gpt-icon.png";
|
||||
}
|
||||
|
||||
const synthesis = (text) => {
|
||||
console.log(text)
|
||||
ElMessage.info("语音合成功能暂不可用")
|
||||
}
|
||||
console.log(text);
|
||||
ElMessage.info("语音合成功能暂不可用");
|
||||
};
|
||||
|
||||
// 重新生成
|
||||
const reGenerate = (prompt) => {
|
||||
console.log(prompt)
|
||||
emits('regen', prompt)
|
||||
}
|
||||
console.log(prompt);
|
||||
emits("regen", prompt);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@@ -191,11 +207,12 @@ const reGenerate = (prompt) => {
|
||||
.chat-page,.chat-export {
|
||||
.chat-line-reply-list {
|
||||
justify-content: center;
|
||||
background-color: rgba(247, 247, 248, 1);
|
||||
background-color: var(--chat-list-bg);
|
||||
color:var(--theme-text-color-primary);
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
border-bottom: 0.5px solid var(--el-border-color);
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
@@ -209,7 +226,7 @@ const reGenerate = (prompt) => {
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
border-radius: 50%;
|
||||
padding: 1px;
|
||||
}
|
||||
}
|
||||
@@ -224,7 +241,7 @@ const reGenerate = (prompt) => {
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 0
|
||||
color #374151;
|
||||
color:var(--theme-text-color-primary);
|
||||
font-size: var(--content-font-size);
|
||||
border-radius: 5px;
|
||||
overflow auto;
|
||||
@@ -238,7 +255,7 @@ const reGenerate = (prompt) => {
|
||||
line-height 1.5
|
||||
|
||||
code {
|
||||
color #374151
|
||||
color:var(--theme-text-color-primary);
|
||||
background-color #e7e7e8
|
||||
padding 0 3px;
|
||||
border-radius 5px;
|
||||
@@ -296,7 +313,8 @@ const reGenerate = (prompt) => {
|
||||
color #212529
|
||||
border-collapse collapse;
|
||||
border 1px solid #dee2e6;
|
||||
background-color #ffffff
|
||||
background-color:var(--chat-content-bg);
|
||||
color:var(--theme-text-color-primary);
|
||||
|
||||
thead {
|
||||
th {
|
||||
@@ -396,10 +414,13 @@ const reGenerate = (prompt) => {
|
||||
min-height 20px;
|
||||
word-break break-word;
|
||||
padding: 1rem
|
||||
color #374151;
|
||||
color var(--theme-text-primary);
|
||||
|
||||
font-size: var(--content-font-size);
|
||||
overflow auto;
|
||||
background-color #F5F5F5
|
||||
// background-color #F5F5F5
|
||||
background-color :var(--chat-content-bg);
|
||||
|
||||
border-radius: 0 10px 10px 10px;
|
||||
|
||||
img {
|
||||
@@ -411,7 +432,7 @@ const reGenerate = (prompt) => {
|
||||
line-height 1.5
|
||||
|
||||
code {
|
||||
color #374151
|
||||
color:var(--theme-text-color-primary);
|
||||
background-color #e7e7e8
|
||||
padding 0 3px;
|
||||
border-radius 5px;
|
||||
@@ -469,7 +490,8 @@ const reGenerate = (prompt) => {
|
||||
color #212529
|
||||
border-collapse collapse;
|
||||
border 1px solid #dee2e6;
|
||||
background-color #ffffff
|
||||
background-color:var(--chat-content-bg);
|
||||
color:var(--theme-text-color-primary);
|
||||
|
||||
thead {
|
||||
th {
|
||||
@@ -541,5 +563,4 @@ const reGenerate = (prompt) => {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -2,22 +2,27 @@
|
||||
<el-container class="chat-file-list">
|
||||
<div v-for="file in fileList" :key="file.url">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover"/>
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
<div class="action">
|
||||
<el-icon @click="removeFile(file)"><CircleCloseFilled /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="title">
|
||||
<el-link :href="file.url" target="_blank" style="--el-font-weight-primary:bold">{{substr(file.name, 30)}}</el-link>
|
||||
<el-link
|
||||
:href="file.url"
|
||||
target="_blank"
|
||||
style="--el-font-weight-primary: bold"
|
||||
>{{ substr(file.name, 30) }}</el-link
|
||||
>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span>{{GetFileType(file.ext)}}</span>
|
||||
<span>{{FormatFileSize(file.size)}}</span>
|
||||
<span>{{ GetFileType(file.ext) }}</span>
|
||||
<span>{{ FormatFileSize(file.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action">
|
||||
@@ -29,26 +34,28 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {CircleCloseFilled} from "@element-plus/icons-vue";
|
||||
import {isImage, removeArrayItem, substr} from "@/utils/libs";
|
||||
import {FormatFileSize, GetFileIcon, GetFileType} from "@/store/system";
|
||||
import { ref } from "vue";
|
||||
import { CircleCloseFilled } from "@element-plus/icons-vue";
|
||||
import { isImage, removeArrayItem, substr } from "@/utils/libs";
|
||||
import { FormatFileSize, GetFileIcon, GetFileType } from "@/store/system";
|
||||
|
||||
const props = defineProps({
|
||||
files: {
|
||||
type: Array,
|
||||
default:[],
|
||||
default: []
|
||||
}
|
||||
})
|
||||
const emits = defineEmits(['removeFile']);
|
||||
const fileList = ref(props.files)
|
||||
|
||||
});
|
||||
const emits = defineEmits(["removeFile"]);
|
||||
const fileList = ref(props.files);
|
||||
|
||||
const removeFile = (file) => {
|
||||
fileList.value = removeArrayItem(fileList.value, file, (v1,v2) => v1.url===v2.url)
|
||||
emits('removeFile', file)
|
||||
}
|
||||
|
||||
fileList.value = removeArrayItem(
|
||||
fileList.value,
|
||||
file,
|
||||
(v1, v2) => v1.url === v2.url
|
||||
);
|
||||
emits("removeFile", file);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
@@ -75,7 +82,8 @@ const removeFile = (file) => {
|
||||
display flex
|
||||
flex-flow row
|
||||
border-radius 10px
|
||||
background-color #ffffff
|
||||
background-color:var(--chat-content-bg);
|
||||
color:var(--theme-text-color-primary);
|
||||
border 1px solid #e3e3e3
|
||||
padding 6px
|
||||
margin-right 10px
|
||||
@@ -112,5 +120,4 @@ const removeFile = (file) => {
|
||||
font-size 20px
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,27 +4,32 @@
|
||||
<i class="iconfont icon-attachment-st"></i>
|
||||
</a>
|
||||
<el-dialog
|
||||
class="file-list-dialog"
|
||||
v-model="show"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="true"
|
||||
:width="800"
|
||||
title="文件管理"
|
||||
class="file-list-dialog"
|
||||
v-model="show"
|
||||
:close-on-click-modal="true"
|
||||
:show-close="true"
|
||||
:width="800"
|
||||
title="文件管理"
|
||||
>
|
||||
<el-scrollbar ref="scrollbarRef" max-height="80vh" style="height: 100%;" @scroll="onScroll">
|
||||
<el-scrollbar
|
||||
ref="scrollbarRef"
|
||||
max-height="80vh"
|
||||
style="height: 100%"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<div class="file-list">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="3">
|
||||
<div class="grid-content">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="afterRead"
|
||||
accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf,.mp4,.mp3"
|
||||
class="avatar-uploader"
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="afterRead"
|
||||
accept=".doc,.docx,.jpg,.png,.jpeg,.xls,.xlsx,.ppt,.pptx,.pdf,.mp4,.mp3"
|
||||
>
|
||||
<el-icon class="avatar-uploader-icon">
|
||||
<Plus/>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
@@ -32,116 +37,139 @@
|
||||
<el-col :span="3" v-for="file in fileData.items" :key="file.url">
|
||||
<div class="grid-content">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:content="file.name"
|
||||
placement="top">
|
||||
<el-image :src="file.url" fit="cover" v-if="isImage(file.ext)" @click="insertURL(file)"/>
|
||||
<el-image :src="GetFileIcon(file.ext)" fit="cover" v-else @click="insertURL(file)"/>
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
:content="file.name"
|
||||
placement="top"
|
||||
>
|
||||
<el-image
|
||||
:src="file.url"
|
||||
fit="cover"
|
||||
v-if="isImage(file.ext)"
|
||||
@click="insertURL(file)"
|
||||
/>
|
||||
<el-image
|
||||
:src="GetFileIcon(file.ext)"
|
||||
fit="cover"
|
||||
v-else
|
||||
@click="insertURL(file)"
|
||||
/>
|
||||
</el-tooltip>
|
||||
|
||||
<div class="opt">
|
||||
<el-button type="danger" size="small" :icon="Delete" @click="removeFile(file)" circle/>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
:icon="Delete"
|
||||
@click="removeFile(file)"
|
||||
circle
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row justify="center" v-if="!fileData.isLastPage" @click="fetchFiles(fileData.page)">
|
||||
<el-row
|
||||
justify="center"
|
||||
v-if="!fileData.isLastPage"
|
||||
@click="fetchFiles(fileData.page)"
|
||||
>
|
||||
<el-link>加载更多</el-link>
|
||||
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {reactive, ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {httpGet, httpPost} from "@/utils/http";
|
||||
import {Delete, Plus} from "@element-plus/icons-vue";
|
||||
import {isImage, removeArrayItem} from "@/utils/libs";
|
||||
import {GetFileIcon} from "@/store/system";
|
||||
import { reactive, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { Delete, Plus } from "@element-plus/icons-vue";
|
||||
import { isImage, removeArrayItem } from "@/utils/libs";
|
||||
import { GetFileIcon } from "@/store/system";
|
||||
|
||||
const props = defineProps({
|
||||
userId: Number,
|
||||
userId: Number
|
||||
});
|
||||
const emits = defineEmits(['selected']);
|
||||
const show = ref(false)
|
||||
const scrollbarRef = ref(null)
|
||||
const emits = defineEmits(["selected"]);
|
||||
const show = ref(false);
|
||||
const scrollbarRef = ref(null);
|
||||
const fileData = reactive({
|
||||
items:[],
|
||||
items: [],
|
||||
page: 1,
|
||||
isLastPage: true,
|
||||
})
|
||||
isLastPage: true
|
||||
});
|
||||
|
||||
const fetchFiles = (pageNo) => {
|
||||
if(pageNo === 1) show.value = true
|
||||
httpPost("/api/upload/list", { page: pageNo || 1, page_size: 30 }).then(res => {
|
||||
const { items, page, total_page } = res.data
|
||||
if (pageNo === 1) show.value = true;
|
||||
httpPost("/api/upload/list", { page: pageNo || 1, page_size: 30 })
|
||||
.then((res) => {
|
||||
const { items, page, total_page } = res.data;
|
||||
|
||||
if(page === 1){
|
||||
fileData.items = items
|
||||
}else{
|
||||
fileData.items = [...fileData.items, ...items]
|
||||
}
|
||||
if (page === 1) {
|
||||
fileData.items = items;
|
||||
} else {
|
||||
fileData.items = [...fileData.items, ...items];
|
||||
}
|
||||
|
||||
fileData.isLastPage = (page === total_page)
|
||||
fileData.isLastPage = page === total_page;
|
||||
|
||||
if(!fileData.isLastPage){
|
||||
fileData.page = page + 1
|
||||
}
|
||||
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
if (!fileData.isLastPage) {
|
||||
fileData.page = page + 1;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
// el-scrollbar 滚动回调
|
||||
const onScroll = (options) => {
|
||||
const wrapRef = scrollbarRef.value.wrapRef
|
||||
scrollbarRef.value.moveY = wrapRef.scrollTop * 100 / wrapRef.clientHeight
|
||||
scrollbarRef.value.moveX = wrapRef.scrollLeft * 100 / wrapRef.clientWidth
|
||||
const poor = wrapRef.scrollHeight - wrapRef.clientHeight
|
||||
const wrapRef = scrollbarRef.value.wrapRef;
|
||||
scrollbarRef.value.moveY = (wrapRef.scrollTop * 100) / wrapRef.clientHeight;
|
||||
scrollbarRef.value.moveX = (wrapRef.scrollLeft * 100) / wrapRef.clientWidth;
|
||||
const poor = wrapRef.scrollHeight - wrapRef.clientHeight;
|
||||
// 判断滚动到底部 自动加载数据
|
||||
if (options.scrollTop + 2 >= poor && !fileData.isLastPage) {
|
||||
fetchFiles(fileData.page)
|
||||
fetchFiles(fileData.page);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const afterRead = (file) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file.file, file.name);
|
||||
formData.append("file", file.file, file.name);
|
||||
// 执行上传操作
|
||||
httpPost('/api/upload', formData).then((res) => {
|
||||
fileData.items.unshift(res.data)
|
||||
ElMessage.success({message: "上传成功", duration: 500})
|
||||
}).catch((e) => {
|
||||
ElMessage.error('图片上传失败:' + e.message)
|
||||
})
|
||||
httpPost("/api/upload", formData)
|
||||
.then((res) => {
|
||||
fileData.items.unshift(res.data);
|
||||
ElMessage.success({ message: "上传成功", duration: 500 });
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("图片上传失败:" + e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const removeFile = (file) => {
|
||||
httpGet('/api/upload/remove?id=' + file.id).then(() => {
|
||||
fileData.items = removeArrayItem(fileData.items, file, (v1, v2) => {
|
||||
return v1.id === v2.id
|
||||
httpGet("/api/upload/remove?id=" + file.id)
|
||||
.then(() => {
|
||||
fileData.items = removeArrayItem(fileData.items, file, (v1, v2) => {
|
||||
return v1.id === v2.id;
|
||||
});
|
||||
ElMessage.success("文件删除成功!");
|
||||
fetchFiles(1);
|
||||
})
|
||||
ElMessage.success("文件删除成功!")
|
||||
fetchFiles(1)
|
||||
}).catch((e) => {
|
||||
ElMessage.error('文件删除失败:' + e.message)
|
||||
})
|
||||
}
|
||||
.catch((e) => {
|
||||
ElMessage.error("文件删除失败:" + e.message);
|
||||
});
|
||||
};
|
||||
|
||||
const insertURL = (file) => {
|
||||
show.value = false
|
||||
show.value = false;
|
||||
// 如果是相对路径,处理成绝对路径
|
||||
if (file.url.indexOf("http") === -1) {
|
||||
file.url = location.protocol + "//" + location.host + file.url
|
||||
file.url = location.protocol + "//" + location.host + file.url;
|
||||
}
|
||||
emits('selected', file)
|
||||
}
|
||||
emits("selected", file);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@@ -149,7 +177,7 @@ const insertURL = (file) => {
|
||||
.file-select-box {
|
||||
.file-upload-img {
|
||||
.iconfont {
|
||||
font-size: 24px;
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,4 +243,4 @@ const insertURL = (file) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,9 @@
|
||||
|
||||
<div class="list-box">
|
||||
<ul>
|
||||
<li v-for="item in samples" :key="item"><a @click="send(item)">{{ item }}</a></li>
|
||||
<li v-for="item in samples" :key="item">
|
||||
<a @click="send(item)">{{ item }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -27,7 +29,9 @@
|
||||
|
||||
<div class="list-box">
|
||||
<ul>
|
||||
<li v-for="item in plugins" :key="item.value"><a @click="send(item.value)">{{ item.text }}</a></li>
|
||||
<li v-for="item in plugins" :key="item.value">
|
||||
<a @click="send(item.value)">{{ item.text }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,19 +58,18 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getSystemInfo } from "@/store/cache";
|
||||
|
||||
import {onMounted, ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {getSystemInfo} from "@/store/cache";
|
||||
|
||||
const title = ref(process.env.VUE_APP_TITLE)
|
||||
const version = ref(process.env.VUE_APP_VERSION)
|
||||
const title = ref(process.env.VUE_APP_TITLE);
|
||||
const version = ref(process.env.VUE_APP_VERSION);
|
||||
|
||||
const samples = ref([
|
||||
"用小学生都能听懂的术语解释什么是量子纠缠",
|
||||
"能给一位6岁男孩的生日会提供一些创造性的建议吗?",
|
||||
"如何用 Go 语言实现支持代理 Http client 请求?"
|
||||
])
|
||||
]);
|
||||
|
||||
const plugins = ref([
|
||||
{
|
||||
@@ -81,7 +84,7 @@ const plugins = ref([
|
||||
value: "今日头条",
|
||||
text: "今日头条:给用户推荐当天的头条新闻,周榜热文"
|
||||
}
|
||||
])
|
||||
]);
|
||||
|
||||
const capabilities = ref([
|
||||
{
|
||||
@@ -96,20 +99,22 @@ const capabilities = ref([
|
||||
text: "绘画:马斯克开拖拉机,20世纪,中国农村。3:2",
|
||||
value: "绘画:马斯克开拖拉机,20世纪,中国农村。3:2"
|
||||
}
|
||||
])
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
getSystemInfo().then(res => {
|
||||
title.value = res.data.title
|
||||
}).catch(e => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message)
|
||||
})
|
||||
})
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
title.value = res.data.title;
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message);
|
||||
});
|
||||
});
|
||||
|
||||
const emits = defineEmits(['send']);
|
||||
const emits = defineEmits(["send"]);
|
||||
const send = (text) => {
|
||||
emits('send', text)
|
||||
}
|
||||
emits("send", text);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="stylus">
|
||||
.welcome {
|
||||
@@ -148,10 +153,9 @@ const send = (text) => {
|
||||
font-size 14px;
|
||||
padding .75rem
|
||||
border-radius 5px;
|
||||
background-color: rgba(247, 247, 248, 1);
|
||||
|
||||
background-color: var(--chat-wel-bg);
|
||||
color:var( --theme-text-color-secondary);
|
||||
line-height 1.5
|
||||
color #666666
|
||||
|
||||
a {
|
||||
cursor pointer
|
||||
@@ -165,4 +169,4 @@ const send = (text) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
<template>
|
||||
<div :class="'admin-header '+theme">
|
||||
<div :class="'admin-header ' + theme">
|
||||
<!-- 折叠按钮 -->
|
||||
<div class="collapse-btn" @click="collapseChange">
|
||||
<el-icon v-if="sidebar.collapse">
|
||||
<Expand/>
|
||||
<Expand />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<Fold/>
|
||||
<Fold />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb">
|
||||
<el-breadcrumb :separator-icon="ArrowRight">
|
||||
<el-breadcrumb-item v-for="item in breadcrumb">{{ item.title }}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-for="item in breadcrumb">{{
|
||||
item.title
|
||||
}}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-user-con">
|
||||
<!-- 切换主题 -->
|
||||
<el-switch
|
||||
style="margin-right: 10px"
|
||||
v-model="dark"
|
||||
inline-prompt
|
||||
:active-action-icon="Moon"
|
||||
:inactive-action-icon="Sunny"
|
||||
@change="changeTheme"
|
||||
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">
|
||||
<el-avatar class="user-avatar" :size="30" :src="avatar"/>
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down/>
|
||||
</el-icon>
|
||||
</span>
|
||||
<span class="el-dropdown-link">
|
||||
<el-avatar class="user-avatar" :size="30" :src="avatar" />
|
||||
<el-icon class="el-icon--right">
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
@@ -48,82 +50,91 @@
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {onMounted, ref, watch} from 'vue';
|
||||
import {getMenuItems, useSidebarStore} from '@/store/sidebar';
|
||||
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";
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { getMenuItems, useSidebarStore } from "@/store/sidebar";
|
||||
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";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
|
||||
const version = ref(process.env.VUE_APP_VERSION)
|
||||
const avatar = ref('/images/user-info.jpg')
|
||||
const version = ref(process.env.VUE_APP_VERSION);
|
||||
const avatar = ref("/images/user-info.jpg");
|
||||
const sidebar = useSidebarStore();
|
||||
const router = useRouter();
|
||||
const breadcrumb = ref([])
|
||||
const breadcrumb = ref([]);
|
||||
|
||||
const store = useSharedStore()
|
||||
const dark = ref(store.adminTheme === 'dark')
|
||||
const theme = ref(store.adminTheme)
|
||||
watch(() => store.adminTheme, (val) => {
|
||||
theme.value = val
|
||||
})
|
||||
const store = useSharedStore();
|
||||
const dark = ref(store.adminTheme === "dark");
|
||||
const theme = ref(store.adminTheme);
|
||||
watch(
|
||||
() => store.adminTheme,
|
||||
(val) => {
|
||||
theme.value = val;
|
||||
}
|
||||
);
|
||||
|
||||
const changeTheme = () => {
|
||||
store.setAdminTheme(dark.value ? 'dark' : 'light')
|
||||
}
|
||||
store.setAdminTheme(dark.value ? "dark" : "light");
|
||||
};
|
||||
|
||||
router.afterEach((to) => {
|
||||
initBreadCrumb(to.path)
|
||||
initBreadCrumb(to.path);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initBreadCrumb(router.currentRoute.value.path)
|
||||
})
|
||||
initBreadCrumb(router.currentRoute.value.path);
|
||||
});
|
||||
|
||||
// 初始化面包屑导航
|
||||
const initBreadCrumb = (path) => {
|
||||
breadcrumb.value = [{title: "首页"}]
|
||||
const items = getMenuItems()
|
||||
breadcrumb.value = [{ title: "首页" }];
|
||||
const items = getMenuItems();
|
||||
if (items) {
|
||||
let bk = false
|
||||
let bk = false;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].index === path) {
|
||||
breadcrumb.value.push({
|
||||
title: items[i].title,
|
||||
path: items[i].index
|
||||
})
|
||||
break
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (bk) {
|
||||
break
|
||||
break;
|
||||
}
|
||||
|
||||
if (items[i]['subs']) {
|
||||
const subs = items[i]['subs']
|
||||
if (items[i]["subs"]) {
|
||||
const subs = items[i]["subs"];
|
||||
for (let j = 0; j < subs.length; j++) {
|
||||
if (subs[j].index === path) {
|
||||
breadcrumb.value.push({
|
||||
title: items[i].title,
|
||||
path: items[i].index
|
||||
})
|
||||
});
|
||||
breadcrumb.value.push({
|
||||
title: subs[j].title,
|
||||
path: subs[j].index
|
||||
})
|
||||
bk = true
|
||||
break
|
||||
});
|
||||
bk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 侧边栏折叠
|
||||
const collapseChange = () => {
|
||||
@@ -137,13 +148,15 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
const logout = function () {
|
||||
httpGet("/api/admin/logout").then(() => {
|
||||
removeAdminToken()
|
||||
router.replace('/admin/login')
|
||||
}).catch((e) => {
|
||||
ElMessage.error("注销失败: " + e.message);
|
||||
})
|
||||
}
|
||||
httpGet("/api/admin/logout")
|
||||
.then(() => {
|
||||
removeAdminToken();
|
||||
router.replace("/admin/login");
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("注销失败: " + e.message);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="stylus">
|
||||
.admin-header {
|
||||
@@ -152,8 +165,8 @@ const logout = function () {
|
||||
overflow hidden
|
||||
height: 50px;
|
||||
font-size: 22px;
|
||||
color: #303133;
|
||||
background-color #ffffff
|
||||
background-color:var(--chat-content-bg);
|
||||
color:var(--theme-text-color-primary);
|
||||
|
||||
.collapse-btn {
|
||||
display: flex;
|
||||
@@ -260,5 +273,4 @@ const logout = function () {
|
||||
.admin-header {
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="mobile-message-mj">
|
||||
<div class="chat-icon">
|
||||
<van-image :src="icon"/>
|
||||
<van-image :src="icon" />
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
@@ -11,21 +11,30 @@
|
||||
<div class="content-inner">
|
||||
<div class="text" v-html="data.html"></div>
|
||||
<div class="images" v-if="data.image?.url !== ''">
|
||||
<el-image :src="data.image?.url"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[data.image?.url]"
|
||||
fit="cover"
|
||||
:initial-index="0" loading="lazy">
|
||||
<el-image
|
||||
:src="data.image?.url"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[data.image?.url]"
|
||||
fit="cover"
|
||||
:initial-index="0"
|
||||
loading="lazy"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="image-slot"
|
||||
:style="{height: height+'px', lineHeight:height+'px'}">
|
||||
正在加载图片<span class="dot">...</span></div>
|
||||
<div
|
||||
class="image-slot"
|
||||
:style="{
|
||||
height: height + 'px',
|
||||
lineHeight: height + 'px'
|
||||
}"
|
||||
>
|
||||
正在加载图片<span class="dot">...</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture/>
|
||||
<Picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
@@ -33,7 +42,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="opt" v-if="data.showOpt &&data.image?.hash !== ''">
|
||||
<div class="opt" v-if="data.showOpt && data.image?.hash !== ''">
|
||||
<div class="opt-line">
|
||||
<ul>
|
||||
<li><a @click="upscale(1)">U1</a></li>
|
||||
@@ -54,17 +63,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, watch} from "vue";
|
||||
import {Picture} from "@element-plus/icons-vue";
|
||||
import {httpPost} from "@/utils/http";
|
||||
import {getSessionId} from "@/store/session";
|
||||
import {showNotify} from "vant";
|
||||
import { ref, watch } from "vue";
|
||||
import { Picture } from "@element-plus/icons-vue";
|
||||
import { httpPost } from "@/utils/http";
|
||||
import { getSessionId } from "@/store/session";
|
||||
import { showNotify } from "vant";
|
||||
|
||||
const props = defineProps({
|
||||
content: Object,
|
||||
@@ -74,36 +82,40 @@ const props = defineProps({
|
||||
createdAt: String
|
||||
});
|
||||
|
||||
const data = ref(props.content)
|
||||
const cacheKey = "img_placeholder_height"
|
||||
const data = ref(props.content);
|
||||
const cacheKey = "img_placeholder_height";
|
||||
const item = localStorage.getItem(cacheKey);
|
||||
const loading = ref(false)
|
||||
const height = ref(0)
|
||||
const loading = ref(false);
|
||||
const height = ref(0);
|
||||
if (item) {
|
||||
height.value = parseInt(item)
|
||||
height.value = parseInt(item);
|
||||
}
|
||||
if (data.value["image"]?.width > 0) {
|
||||
height.value = 350 * data.value["image"]?.height / data.value["image"]?.width
|
||||
localStorage.setItem(cacheKey, height.value)
|
||||
height.value =
|
||||
(350 * data.value["image"]?.height) / data.value["image"]?.width;
|
||||
localStorage.setItem(cacheKey, height.value);
|
||||
}
|
||||
data.value["showOpt"] = data.value["content"]?.indexOf("- Image #") === -1;
|
||||
// console.log(data.value)
|
||||
|
||||
watch(() => props.content, (newVal) => {
|
||||
data.value = newVal;
|
||||
});
|
||||
const emits = defineEmits(['disable-input', 'disable-input']);
|
||||
watch(
|
||||
() => props.content,
|
||||
(newVal) => {
|
||||
data.value = newVal;
|
||||
}
|
||||
);
|
||||
const emits = defineEmits(["disable-input", "disable-input"]);
|
||||
const upscale = (index) => {
|
||||
send('/api/mj/upscale', index)
|
||||
}
|
||||
send("/api/mj/upscale", index);
|
||||
};
|
||||
|
||||
const variation = (index) => {
|
||||
send('/api/mj/variation', index)
|
||||
}
|
||||
send("/api/mj/variation", index);
|
||||
};
|
||||
|
||||
const send = (url, index) => {
|
||||
loading.value = true
|
||||
emits('disable-input')
|
||||
loading.value = true;
|
||||
emits("disable-input");
|
||||
httpPost(url, {
|
||||
index: index,
|
||||
src: "chat",
|
||||
@@ -114,15 +126,20 @@ const send = (url, index) => {
|
||||
prompt: data.value?.["prompt"],
|
||||
chat_id: props.chatId,
|
||||
role_id: props.roleId,
|
||||
icon: props.icon,
|
||||
}).then(() => {
|
||||
showNotify({type: "success", message: "任务推送成功,请耐心等待任务执行..."})
|
||||
loading.value = false
|
||||
}).catch(e => {
|
||||
showNotify({type: "danger", message: "任务推送失败:" + e.message})
|
||||
emits('disable-input')
|
||||
icon: props.icon
|
||||
})
|
||||
}
|
||||
.then(() => {
|
||||
showNotify({
|
||||
type: "success",
|
||||
message: "任务推送成功,请耐心等待任务执行..."
|
||||
});
|
||||
loading.value = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: "danger", message: "任务推送失败:" + e.message });
|
||||
emits("disable-input");
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@@ -268,4 +285,4 @@ const send = (url, index) => {
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user