merge v4.2.0

This commit is contained in:
RockYang
2025-11-11 10:17:03 +08:00
34 changed files with 2631 additions and 556 deletions

View File

@@ -6,7 +6,7 @@ VUE_APP_ADMIN_USER=admin
VUE_APP_ADMIN_PASS=admin123
VUE_APP_KEY_PREFIX=GeekAI_DEV_
VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.1.9
VUE_APP_VERSION=v4.2.0
VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GITHUB_URL=https://github.com/yangjian102621/geekai
VUE_APP_GITEE_URL=https://gitee.com/blackfox/geekai

View File

@@ -1,8 +1,8 @@
VUE_APP_API_HOST=
VUE_APP_WS_HOST=
VUE_APP_KEY_PREFIX=GeekAI_
VUE_APP_VERSION=v4.2.0
VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.1.9
VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GITHUB_URL=https://github.com/yangjian102621/geekai
VUE_APP_GITEE_URL=https://gitee.com/blackfox/geekai

View File

@@ -92,6 +92,29 @@ const connect = () => {
});
store.setSocket(_socket);
};
// 打印 banner
const banner = `
.oooooo. oooo .o. ooooo
d8P' 'Y8b 888 .888. 888
888 .ooooo. .ooooo. 888 oooo .8"888. 888
888 d88' 88b d88' 88b 888 .8P' .8' 888. 888
888 ooooo 888ooo888 888ooo888 888888. .88ooo8888. 888
'88. .88' 888 .o 888 .o 888 88b. .8' 888. 888
Y8bood8P' Y8bod8P' Y8bod8P' o888o o888o o88o o8888o o888o
`;
console.log("%c" + banner + "", "color: purple;font-size: 18px;");
console.log("%c感谢大家为 GeekAI 做出的卓越贡献!", "color: green;font-size: 40px;font-family: '微软雅黑';");
console.log(
"%c项目源码https://github.com/yangjian102621/geekai %c 您的 star 对我们非常重要!",
"color: green;font-size: 20px;font-family: '微软雅黑';",
"color: red;font-size: 20px;font-family: '微软雅黑';"
);
console.log("%c 愿你出走半生,归来仍是少年!大奉武夫许七安,前来凿阵!", "color: #7c39ed;font-size: 18px;font-family: '微软雅黑';");
</script>
<style lang="stylus">

View File

@@ -132,11 +132,6 @@
overflow: hidden;
border-radius: 50%;
font-size: 20px;
// img{
// width: 24px;
// height: 24px;
// }
}
&.active {
@@ -183,19 +178,6 @@
}
}
::v-deep(.theme-box) {
position: relative !important;
right: initial;
bottom: initial;
width: 20px;
height: 20px;
line-height: 18px;
.iconfont {
font-size: 15px !important;
}
}
.right-main {
height: 100%;
// background: #f5f7fd;
@@ -246,18 +228,18 @@
}
&:hover {
background: rgba(79, 89, 102, 0.1);
background: rgba(183, 176, 255, 0.5);
}
}
li.active {
background: rgba(79, 89, 102, 0.1);
background: rgba(183, 176, 255, 0.5);
}
}
.setting-menus {
.title {
color: #222226;
color: var(--text-theme-color);
}
.el-icon,
@@ -265,7 +247,7 @@
font-size: 18px
margin-right: 6px
}
color: #222226;
color: var(--text-theme-color);
}
.username {

View File

@@ -1,238 +1,239 @@
.chat-line {
ol, ul {
margin: 0.8em 0;
list-style: normal;
}
a {
color :var(--a-link-color);
text-decoration: underline;
padding: 0 2px;
}
.chat-line,
.notice-dialog {
ol,
ul {
margin: 0.8em 0;
list-style: normal;
}
a {
color: var(--a-link-color);
text-decoration: underline;
h1,
h2,
h3,
h4,
h5,
h6 {
position: relative;
margin-top: 1rem;
margin-bottom: 1rem;
font-weight: bold;
line-height: 1.4;
cursor: text;
}
padding: 0 2px;
}
h1:hover a.anchor,
h2:hover a.anchor,
h3:hover a.anchor,
h4:hover a.anchor,
h5:hover a.anchor,
h6:hover a.anchor {
text-decoration: none;
}
h1,
h2,
h3,
h4,
h5,
h6 {
position: relative;
margin-top: 1rem;
margin-bottom: 1rem;
font-weight: bold;
line-height: 1.4;
cursor: text;
}
h1 tt,
h1 code {
font-size: inherit !important;
}
h1:hover a.anchor,
h2:hover a.anchor,
h3:hover a.anchor,
h4:hover a.anchor,
h5:hover a.anchor,
h6:hover a.anchor {
text-decoration: none;
}
h2 tt,
h2 code {
font-size: inherit !important;
}
h1 tt,
h1 code {
font-size: inherit !important;
}
h3 tt,
h3 code {
font-size: inherit !important;
}
h2 tt,
h2 code {
font-size: inherit !important;
}
h4 tt,
h4 code {
font-size: inherit !important;
}
h3 tt,
h3 code {
font-size: inherit !important;
}
h5 tt,
h5 code {
font-size: inherit !important;
}
h4 tt,
h4 code {
font-size: inherit !important;
}
h6 tt,
h6 code {
font-size: inherit !important;
}
h5 tt,
h5 code {
font-size: inherit !important;
}
h2 a,
h3 a {
color: #34495e;
}
h6 tt,
h6 code {
font-size: inherit !important;
}
h1 {
padding-bottom: .4rem;
font-size: 2.2rem;
line-height: 1.3;
}
h2 a,
h3 a {
color: #34495e;
}
h2 {
font-size: 1.75rem;
line-height: 1.225;
margin: 35px 0 15px;
padding-bottom: 0.5em;
border-bottom: 1px solid #ddd;
}
h1 {
padding-bottom: 0.4rem;
font-size: 2.2rem;
line-height: 1.3;
}
h3 {
font-size: 1.4rem;
line-height: 1.43;
margin: 20px 0 7px;
}
h2 {
font-size: 1.75rem;
line-height: 1.225;
margin: 35px 0 15px;
padding-bottom: 0.5em;
border-bottom: 1px solid #ddd;
}
h4 {
font-size: 1.2rem;
}
h3 {
font-size: 1.4rem;
line-height: 1.43;
margin: 20px 0 7px;
}
h5 {
font-size: 1rem;
}
h4 {
font-size: 1.2rem;
}
h6 {
font-size: 1rem;
color: #777;
}
h5 {
font-size: 1rem;
}
p,
blockquote,
ul,
ol,
dl,
table {
margin: 0.8em 0;
}
h6 {
font-size: 1rem;
color: #777;
}
li > ol,
li > ul {
margin: 0 0;
}
p,
blockquote,
ul,
ol,
dl,
table {
margin: 0.8em 0;
}
hr {
height: 2px;
padding: 0;
margin: 16px 0;
background-color: #e7e7e7;
border: 0 none;
overflow: hidden;
box-sizing: content-box;
}
li > ol,
li > ul {
margin: 0 0;
}
body > h2:first-child {
margin-top: 0;
padding-top: 0;
}
hr {
height: 2px;
padding: 0;
margin: 16px 0;
background-color: #e7e7e7;
border: 0 none;
overflow: hidden;
box-sizing: content-box;
}
body > h1:first-child {
margin-top: 0;
padding-top: 0;
}
body > h2:first-child {
margin-top: 0;
padding-top: 0;
}
body > h1:first-child + h2 {
margin-top: 0;
padding-top: 0;
}
body > h1:first-child {
margin-top: 0;
padding-top: 0;
}
body > h3:first-child,
body > h4:first-child,
body > h5:first-child,
body > h6:first-child {
margin-top: 0;
padding-top: 0;
}
body > h1:first-child + h2 {
margin-top: 0;
padding-top: 0;
}
a:first-child h1,
a:first-child h2,
a:first-child h3,
a:first-child h4,
a:first-child h5,
a:first-child h6 {
margin-top: 0;
padding-top: 0;
}
body > h3:first-child,
body > h4:first-child,
body > h5:first-child,
body > h6:first-child {
margin-top: 0;
padding-top: 0;
}
h1 p,
h2 p,
h3 p,
h4 p,
h5 p,
h6 p {
margin-top: 0;
}
a:first-child h1,
a:first-child h2,
a:first-child h3,
a:first-child h4,
a:first-child h5,
a:first-child h6 {
margin-top: 0;
padding-top: 0;
}
li p.first {
display: inline-block;
}
h1 p,
h2 p,
h3 p,
h4 p,
h5 p,
h6 p {
margin-top: 0;
}
ul,
ol {
padding-left: 30px;
}
li p.first {
display: inline-block;
}
ul:first-child,
ol:first-child {
margin-top: 0;
}
ul,
ol {
padding-left: 30px;
}
ul:last-child,
ol:last-child {
margin-bottom: 0;
}
ul:first-child,
ol:first-child {
margin-top: 0;
}
blockquote {
border-left: 4px solid #42b983;
padding: 10px 15px;
color: #777;
background-color: rgba(66, 185, 131, .1);
}
ul:last-child,
ol:last-child {
margin-bottom: 0;
}
table {
padding: 0;
word-break: initial;
}
blockquote {
border-left: 4px solid #42b983;
padding: 10px 15px;
color: #777;
background-color: rgba(66, 185, 131, 0.1);
}
table tr {
border-top: 1px solid #dfe2e5;
margin: 0;
padding: 0;
}
table {
padding: 0;
word-break: initial;
}
table tr:nth-child(2n),
thead {
background-color: #fafafa;
}
table tr {
border-top: 1px solid #dfe2e5;
margin: 0;
padding: 0;
}
table tr th {
font-weight: bold;
border: 1px solid #dfe2e5;
border-bottom: 0;
text-align: left;
margin: 0;
padding: 6px 13px;
}
table tr:nth-child(2n),
thead {
background-color: #fafafa;
}
table tr td {
border: 1px solid #dfe2e5;
text-align: left;
margin: 0;
padding: 6px 13px;
}
table tr th {
font-weight: bold;
border: 1px solid #dfe2e5;
border-bottom: 0;
text-align: left;
margin: 0;
padding: 6px 13px;
}
table tr th:first-child,
table tr td:first-child {
margin-top: 0;
}
table tr td {
border: 1px solid #dfe2e5;
text-align: left;
margin: 0;
padding: 6px 13px;
}
table tr th:last-child,
table tr td:last-child {
margin-bottom: 0;
}
table tr th:first-child,
table tr td:first-child {
margin-top: 0;
}
table tr th:last-child,
table tr td:last-child {
margin-bottom: 0;
}
}

View File

@@ -243,6 +243,9 @@
opacity 0
transform: translate(-50%, 0px);
transition opacity 0.3s ease 0s
display flex
justify-content center
align-items center
}
&:hover {

View File

@@ -55,7 +55,7 @@
//filter: invert(100%);
}
.more-menus span.title{
color:#000;
color: var(--text-theme-color);
}
//
@@ -89,4 +89,7 @@
//
--quote-bg-color: #1F243F;
--quote-text-color: #fff;
// el-dialog
--el-box-shadow: 0 0 15px rgba(107, 80, 225, 0.8);
}

View File

@@ -2,27 +2,25 @@
<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" />
<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>
@@ -35,7 +33,7 @@
<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
><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span
>
<span class="bar-item">tokens: {{ finalTokens }}</span>
</div>
@@ -46,27 +44,25 @@
<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>
@@ -81,7 +77,7 @@
</div>
<div class="bar" v-if="data.created_at > 0">
<span class="bar-item"
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span
>
<!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
</div>
@@ -91,15 +87,15 @@
</template>
<script setup>
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 emoji from 'markdown-it-emoji'
import mathjaxPlugin from 'markdown-it-mathjax3'
import MarkdownIt from 'markdown-it'
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 emoji from "markdown-it-emoji";
import mathjaxPlugin from "markdown-it-mathjax3";
import MarkdownIt from "markdown-it";
const md = new MarkdownIt({
breaks: true,
@@ -107,91 +103,83 @@ const md = new MarkdownIt({
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,
'&lt;/textarea>'
)}</textarea>`
/<\/textarea>/g,
"&lt;/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(emoji)
});
md.use(mathjaxPlugin);
md.use(emoji);
const props = defineProps({
data: {
type: Object,
default: {
content: '',
created_at: '',
content: "",
created_at: "",
tokens: 0,
model: '',
icon: '',
model: "",
icon: "",
},
},
listStyle: {
type: String,
default: 'list',
default: "list",
},
})
const finalTokens = ref(props.data.tokens)
const content = ref(processPrompt(props.data.content))
const files = ref([])
});
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)
const urlPrefix = `${window.location.protocol}//${window.location.host}`
const linkRegex = /(https?:\/\/\S+)/g;
const links = props.data.content.match(linkRegex);
if (links) {
const _links = links.map((link) => {
if (link.startsWith(urlPrefix)) {
return link.replace(urlPrefix, '')
}
return link
})
// 合并数组并去重
const urls = [...new Set([...links, ..._links])]
httpPost('/api/upload/list', { urls: urls })
.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">

View File

@@ -1,26 +1,32 @@
<template>
<div class="theme-box" @click="toggleTheme">
<div class="theme-box" @click="toggleTheme" :class="size">
<i class="iconfont" :class="themePage === 'light' ? 'icon-yueliang' : 'icon-taiyang'"></i>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useSharedStore } from '@/store/sharedata'
import { ref } from "vue";
import { useSharedStore } from "@/store/sharedata";
const props = defineProps({
size: {
type: String,
default: "",
},
});
// 定义主题状态,初始值从 localStorage 获取
const store = useSharedStore()
const themePage = ref(store.theme || 'light')
const store = useSharedStore();
const themePage = ref(store.theme || "light");
// 切换主题函数
const toggleTheme = () => {
themePage.value = themePage.value === 'light' ? 'dark' : 'light'
store.setTheme(themePage.value) // 保存主题
}
themePage.value = themePage.value === "light" ? "dark" : "light";
store.setTheme(themePage.value); // 保存主题
};
</script>
<style lang="stylus" scoped>
@import '@/assets/iconfont/iconfont.css'
.theme-box{
z-index :111
position: fixed;
@@ -49,4 +55,17 @@ const toggleTheme = () => {
transition: transform 0.3s ease;
}
}
.theme-box.small {
position: relative !important;
right: initial;
bottom: initial;
width: 20px;
height: 20px;
line-height: 18px;
.iconfont {
font-size: 15px !important;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<el-dialog class="config-dialog" v-model="showDialog" :close-on-click-modal="true" :before-close="close" style="max-width: 400px" title="账户信息">
<div class="flex-center-col p-4 pt-0" id="user-info">
<div class="flex-center-col pl-4 pr-4" id="user-info">
<user-profile @hide="close" />
</div>
</el-dialog>

View File

@@ -25,6 +25,9 @@
<el-form-item label="剩余算力">
<el-text type="warning">{{ user["power"] }}</el-text>
<el-tag type="info" size="small" class="ml-2 cursor-pointer" @click="gotoLog">算力日志</el-tag>
<el-tooltip :content="`每日签到可获得 ${systemConfig.daily_power} 算力`" placement="top" v-if="systemConfig.daily_power > 0">
<el-button type="primary" size="small" @click="signIn" class="ml-2">签到</el-button>
</el-tooltip>
</el-form-item>
<el-form-item label="会员到期时间" v-if="user['expired_time'] > 0">
<el-tag type="danger">{{ dateFormat(user["expired_time"]) }}</el-tag>
@@ -44,8 +47,9 @@ import { ElMessage } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import Compressor from "compressorjs";
import { dateFormat } from "@/utils/libs";
import { checkSession } from "@/store/cache";
import { checkSession, getSystemInfo } from "@/store/cache";
import { useRouter } from "vue-router";
import { showMessageError, showMessageOK } from "@/utils/dialog";
const user = ref({
vip: false,
username: "演示数据",
@@ -56,6 +60,7 @@ const user = ref({
});
const vipImg = ref("/images/menu/member.png");
const systemConfig = ref({});
const router = useRouter();
const emits = defineEmits(["hide"]);
onMounted(() => {
@@ -73,6 +78,10 @@ onMounted(() => {
.catch((e) => {
console.log(e);
});
getSystemInfo().then((res) => {
systemConfig.value = res.data;
});
});
const afterRead = (file) => {
@@ -112,6 +121,17 @@ const gotoLog = () => {
router.push("/powerLog");
emits("hide", false);
};
const signIn = () => {
httpGet("/api/user/signin")
.then(() => {
showMessageOK("签到成功");
user.value.power += systemConfig.value.daily_power;
})
.catch((e) => {
showMessageError(e.message);
});
};
</script>
<style lang="stylus" scoped>

View File

@@ -8,236 +8,248 @@
/**
* Util lib functions
*/
import {showConfirmDialog} from "vant";
import { showConfirmDialog } from "vant";
// generate a random string
export function randString(length) {
const str = "0123456789abcdefghijklmnopqrstuvwxyz"
const size = str.length
let buf = []
for (let i = 0; i < length; i++) {
const rand = Math.random() * size
buf.push(str.charAt(rand))
}
return buf.join("")
const str = "0123456789abcdefghijklmnopqrstuvwxyz";
const size = str.length;
let buf = [];
for (let i = 0; i < length; i++) {
const rand = Math.random() * size;
buf.push(str.charAt(rand));
}
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);
});
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;
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i;
return mobileRegex.test(userAgent);
const userAgent = navigator.userAgent;
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i;
return mobileRegex.test(userAgent);
}
// 格式化日期
export function dateFormat(timestamp, format) {
if (!timestamp) {
return '';
} else if (timestamp < 9680917502) {
timestamp = timestamp * 1000;
}
let year, month, day, HH, mm, ss;
let time = new Date(timestamp);
let timeDate;
year = time.getFullYear(); // 年
month = time.getMonth() + 1; // 月
day = time.getDate(); // 日
HH = time.getHours(); // 时
mm = time.getMinutes(); // 分
ss = time.getSeconds(); // 秒
if (!timestamp) {
return "";
} else if (timestamp < 9680917502) {
timestamp = timestamp * 1000;
}
let year, month, day, HH, mm, ss;
let time = new Date(timestamp);
let timeDate;
year = time.getFullYear(); // 年
month = time.getMonth() + 1; // 月
day = time.getDate(); // 日
HH = time.getHours(); // 时
mm = time.getMinutes(); // 分
ss = time.getSeconds(); // 秒
month = month < 10 ? '0' + month : month;
day = day < 10 ? '0' + day : day;
HH = HH < 10 ? '0' + HH : HH; // 时
mm = mm < 10 ? '0' + mm : mm; // 分
ss = ss < 10 ? '0' + ss : ss; // 秒
month = month < 10 ? "0" + month : month;
day = day < 10 ? "0" + day : day;
HH = HH < 10 ? "0" + HH : HH; // 时
mm = mm < 10 ? "0" + mm : mm; // 分
ss = ss < 10 ? "0" + ss : ss; // 秒
switch (format) {
case 'yyyy':
timeDate = String(year);
break;
case 'yyyy-MM':
timeDate = year + '-' + month;
break;
case 'yyyy-MM-dd':
timeDate = year + '-' + month + '-' + day;
break;
case 'yyyy/MM/dd':
timeDate = year + '/' + month + '/' + day;
break;
case 'yyyy-MM-dd HH:mm:ss':
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss;
break;
case 'HH:mm:ss':
timeDate = HH + ':' + mm + ':' + ss;
break;
case 'MM':
timeDate = String(month);
break;
default:
timeDate = year + '-' + month + '-' + day + ' ' + HH + ':' + mm + ':' + ss;
break;
}
return timeDate;
switch (format) {
case "yyyy":
timeDate = String(year);
break;
case "yyyy-MM":
timeDate = year + "-" + month;
break;
case "yyyy-MM-dd":
timeDate = year + "-" + month + "-" + day;
break;
case "yyyy/MM/dd":
timeDate = year + "/" + month + "/" + day;
break;
case "yyyy-MM-dd HH:mm:ss":
timeDate = year + "-" + month + "-" + day + " " + HH + ":" + mm + ":" + ss;
break;
case "HH:mm:ss":
timeDate = HH + ":" + mm + ":" + ss;
break;
case "MM":
timeDate = String(month);
break;
default:
timeDate = year + "-" + month + "-" + day + " " + HH + ":" + mm + ":" + ss;
break;
}
return timeDate;
}
export function formatTime(time) {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
}
// 判断数组中是否包含某个元素
export function arrayContains(array, value, compare) {
if (!array) {
return false
}
if (typeof compare !== 'function') {
compare = function (v1, v2) {
return v1 === v2;
}
}
for (let i = 0; i < array.length; i++) {
if (compare(array[i], value)) {
return true;
}
}
if (!array) {
return false;
}
if (typeof compare !== "function") {
compare = function (v1, v2) {
return v1 === v2;
};
}
for (let i = 0; i < array.length; i++) {
if (compare(array[i], value)) {
return true;
}
}
return false;
}
// 删除数组中指定的元素
export function removeArrayItem(array, value, compare) {
if (typeof compare !== 'function') {
compare = function (v1, v2) {
return v1 === v2;
}
if (typeof compare !== "function") {
compare = function (v1, v2) {
return v1 === v2;
};
}
for (let i = 0; i < array.length; i++) {
if (compare(array[i], value)) {
array.splice(i, 1);
break;
}
for (let i = 0; i < array.length; i++) {
if (compare(array[i], value)) {
array.splice(i, 1);
break;
}
}
return array;
}
return array;
}
// 渲染输入的换行符
export function renderInputText(text) {
const replaceRegex = /(\n\r|\r\n|\r|\n)/g;
text = text || '';
return text.replace(replaceRegex, "<br/>");
const replaceRegex = /(\n\r|\r\n|\r|\n)/g;
text = text || "";
return text.replace(replaceRegex, "<br/>");
}
// 拷贝对象
export function copyObj(origin) {
return JSON.parse(JSON.stringify(origin));
return JSON.parse(JSON.stringify(origin));
}
export function disabledDate(time) {
return time.getTime() < Date.now()
return time.getTime() < Date.now();
}
// 字符串截取
export function substr(str, length) {
let result = ''
let count = 0
let result = "";
let count = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charAt(i)
const charCode = str.charCodeAt(i);
for (let i = 0; i < str.length; i++) {
const char = str.charAt(i);
const charCode = str.charCodeAt(i);
// 判断字符是否为中文字符
if (charCode >= 0x4e00 && charCode <= 0x9fff) {
// 中文字符算两个字符
count += 2
} else {
count++
}
if (count <= length) {
result += char
} else {
result += " ..."
break
}
// 判断字符是否为中文字符
if (charCode >= 0x4e00 && charCode <= 0x9fff) {
// 中文字符算两个字符
count += 2;
} else {
count++;
}
return result
if (count <= length) {
result += char;
} else {
result += " ...";
break;
}
}
return result;
}
export function isImage(url) {
const expr = /\.(jpg|jpeg|png|gif|bmp|svg)$/i;
return expr.test(url);
const expr = /\.(jpg|jpeg|png|gif|bmp|svg)$/i;
return expr.test(url);
}
export function processContent(content) {
if (!content) {
return ""
}
// 如果是图片链接地址,则直接替换成图片标签
const linkRegex = /(https?:\/\/\S+)/g;
const links = content.match(linkRegex);
if (links) {
for (let link of links) {
if (isImage(link)) {
const index = content.indexOf(link)
if (content.substring(index - 1, 2) !== "]") {
content = content.replace(link, "\n![](" + link + ")\n")
}
}
if (!content) {
return "";
}
// 如果是图片链接地址,则直接替换成图片标签
const linkRegex = /(https?:\/\/\S+)/g;
const links = content.match(linkRegex);
if (links) {
for (let link of links) {
if (isImage(link)) {
const index = content.indexOf(link);
if (content.substring(index - 1, 2) !== "]") {
content = content.replace(link, "\n![](" + link + ")\n");
}
}
}
return content
}
// 处理推理标签
if (content.includes("<think>")) {
content = content.replace(/<think>(.*?)<\/think>/gs, (match, content) => {
if (content.length > 10) {
return `<blockquote>${content}</blockquote>`;
}
return "";
});
content = content.replace(/<think>(.*?)$/gs, (match, content) => {
if (content.length > 10) {
return `<blockquote>${content}</blockquote>`;
}
return "";
});
}
return content;
}
export function processPrompt(prompt) {
return prompt.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return prompt.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// 判断是否为微信浏览器
export function isWeChatBrowser() {
return /MicroMessenger/i.test( navigator.userAgent);
return /MicroMessenger/i.test(navigator.userAgent);
}
export function showLoginDialog(router) {
showConfirmDialog({
title: '登录',
message:
'此操作需要登录才能进行,前往登录?',
}).then(() => {
router.push("/mobile/login")
}).catch(() => {
// on cancel
showConfirmDialog({
title: "登录",
message: "此操作需要登录才能进行,前往登录?",
})
.then(() => {
router.push("/mobile/login");
})
.catch(() => {
// on cancel
});
}
export const replaceImg =(img) => {
if (!img.startsWith("http")) {
img = `${location.protocol}//${location.host}/${img}`
}
const devHost = process.env.VUE_APP_API_HOST
const localhost = "http://localhost:5678"
if (img.includes(localhost)) {
return img?.replace(localhost, devHost)
}
return img
}
export const replaceImg = (img) => {
if (!img.startsWith("http")) {
img = `${location.protocol}//${location.host}/${img}`;
}
const devHost = process.env.VUE_APP_API_HOST;
const localhost = "http://localhost:5678";
if (img.includes(localhost)) {
return img?.replace(localhost, devHost);
}
return img;
};
export function isChrome() {
const userAgent = navigator.userAgent.toLowerCase();
return /chrome/.test(userAgent) && !/edg/.test(userAgent);
const userAgent = navigator.userAgent.toLowerCase();
return /chrome/.test(userAgent) && !/edg/.test(userAgent);
}

View File

@@ -3,7 +3,7 @@
<el-container>
<el-aside v-show="store.chatListExtend">
<div class="flex w-full justify-center pt-3 pb-3">
<img :src="logo" style="max-height: 40px" :alt="title" v-if="logo !== ''"/>
<img :src="logo" style="max-height: 40px" :alt="title" v-if="logo !== ''" />
<h2 v-else>{{ title }}</h2>
</div>
@@ -194,7 +194,7 @@
</el-container>
<el-dialog v-model="showNotice" :show-close="true" class="notice-dialog" title="网站公告">
<div class="notice p-4">
<div class="notice">
<div v-html="notice"></div>
</div>
@@ -232,24 +232,24 @@
</div>
</template>
<script setup>
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import ChatPrompt from "@/components/ChatPrompt.vue";
import ChatReply from "@/components/ChatReply.vue";
import {Delete, Edit, InfoFilled, More, Promotion, Search, Share, VideoPause} from "@element-plus/icons-vue";
import { Delete, Edit, InfoFilled, More, Promotion, Search, Share, VideoPause } from "@element-plus/icons-vue";
import "highlight.js/styles/a11y-dark.css";
import {isMobile, randString, removeArrayItem, UUID} from "@/utils/libs";
import {ElMessage, ElMessageBox} from "element-plus";
import {httpGet, httpPost} from "@/utils/http";
import {useRouter} from "vue-router";
import { isMobile, randString, removeArrayItem, UUID } from "@/utils/libs";
import { ElMessage, ElMessageBox } from "element-plus";
import { httpGet, httpPost } from "@/utils/http";
import { useRouter } from "vue-router";
import Clipboard from "clipboard";
import {checkSession, getClientId, getSystemInfo} from "@/store/cache";
import { checkSession, getClientId, getSystemInfo } from "@/store/cache";
import Welcome from "@/components/Welcome.vue";
import {useSharedStore} from "@/store/sharedata";
import { useSharedStore } from "@/store/sharedata";
import FileSelect from "@/components/FileSelect.vue";
import FileList from "@/components/FileList.vue";
import ChatSetting from "@/components/ChatSetting.vue";
import BackTop from "@/components/BackTop.vue";
import {closeLoading, showLoading, showMessageError} from "@/utils/dialog";
import { closeLoading, showLoading, showMessageError } from "@/utils/dialog";
import MarkdownIt from "markdown-it";
import emoji from "markdown-it-emoji";
@@ -977,6 +977,7 @@ const realtimeChat = () => {
</style>
<style lang="stylus">
@import '@/assets/css/markdown/vue.css';
.notice-dialog {
.el-dialog__header {
padding-bottom 0
@@ -985,6 +986,10 @@ const realtimeChat = () => {
.el-dialog__body {
padding 0 20px
h2 {
margin: 20px 0 15px 0;
}
ol, ul {
padding-left 10px
}
@@ -995,7 +1000,7 @@ const realtimeChat = () => {
}
ul {
list-style disc
list-style inside
}
}
}

View File

@@ -320,6 +320,7 @@ onMounted(() => {
models.value = res.data;
selectedModel.value = models.value[0];
params.value.model_id = selectedModel.value.id;
changeModel(selectedModel.value);
})
.catch((e) => {
showMessageError("获取模型列表失败:" + e.message);

View File

@@ -39,25 +39,25 @@
<el-popover v-if="moreNavs.length > 0" placement="right-end" trigger="hover">
<template #reference>
<li class="menu-list-item flex-center-col">
<i class="iconfont icon-more"/>
<i class="iconfont icon-more" />
</li>
</template>
<template #default>
<ul class="more-menus">
<li
v-for="(item, index) in moreNavs"
:key="item.url"
:class="{
active: item.url === curPath,
moreTitle: index !== 3 && index !== 4,
twoTittle: index === 3 || index === 4,
}"
v-for="(item, index) in moreNavs"
:key="item.url"
:class="{
active: item.url === curPath,
moreTitle: index !== 3 && index !== 4,
twoTittle: index === 3 || index === 4,
}"
>
<a @click="changeNav(item)">
<span v-if="item.icon.startsWith('icon')" class="mr-2">
<i class="iconfont" :class="item.icon"></i>
</span>
<el-image :src="item.icon" style="width: 20px; height: 20px" v-else/>
<span v-if="item.icon.startsWith('icon')" class="mr-2">
<i class="iconfont" :class="item.icon"></i>
</span>
<el-image :src="item.icon" style="width: 20px; height: 20px" v-else />
<span :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</span>
</a>
</li>
@@ -67,7 +67,7 @@
<el-popover placement="right-end" trigger="hover" v-if="loginUser.id">
<template #reference>
<li class="menu-list-item flex-center-col">
<i class="iconfont icon-config"/>
<i class="iconfont icon-config" />
</li>
</template>
<template #default>
@@ -75,7 +75,7 @@
<li>
<div @click="showConfigDialog = true" class="flex">
<el-icon>
<UserFilled/>
<UserFilled />
</el-icon>
<span class="username title">{{ loginUser.nickname }}</span>
</div>
@@ -100,7 +100,7 @@
<i class="iconfont icon-house"></i>
</a>
<div class="pl-1">
<ThemeChange/>
<ThemeChange size="small" />
</div>
</div>
</div>
@@ -133,17 +133,17 @@
</template>
<script setup>
import {UserFilled} from "@element-plus/icons-vue";
import { UserFilled } from "@element-plus/icons-vue";
import ThemeChange from "@/components/ThemeChange.vue";
import {useRouter} from "vue-router";
import {computed, onMounted, ref, watch} from "vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {checkSession, getLicenseInfo, getSystemInfo} from "@/store/cache";
import {removeUserToken} from "@/store/session";
import {useSharedStore} from "@/store/sharedata";
import { useRouter } from "vue-router";
import { computed, onMounted, ref, watch } from "vue";
import { httpGet } from "@/utils/http";
import { ElMessage } from "element-plus";
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
import { removeUserToken } from "@/store/session";
import { useSharedStore } from "@/store/sharedata";
import ConfigDialog from "@/components/UserInfoDialog.vue";
import {showMessageError} from "@/utils/dialog";
import { showMessageError } from "@/utils/dialog";
import LoginDialog from "@/components/LoginDialog.vue";
const router = useRouter();
@@ -188,7 +188,7 @@ const getFirstPathSegment = (url) => {
const stars = computed(() => {
return 1000;
})
});
watch(
() => store.showLoginDialog,

View File

@@ -141,6 +141,7 @@ const types = ref([
{ label: "Suno文生歌", value: "suno" },
{ label: "Luma视频", value: "luma" },
{ label: "Realtime API", value: "realtime" },
{ label: "其他", value: "other" },
]);
const isEdit = ref(false);
const clipboard = ref(null);

View File

@@ -195,7 +195,7 @@ const type = ref([
// 获取 API KEY
const apiKeys = ref([]);
httpGet("/api/admin/apikey/list?type=chat|dalle")
httpGet("/api/admin/apikey/list")
.then((res) => {
apiKeys.value = res.data;
})
@@ -345,13 +345,6 @@ const remove = function (row) {
}
}
.cell {
.copy-model {
margin-left 6px
cursor pointer
}
}
.el-select {
width: 100%
}