mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-12-26 10:05:57 +08:00
merge v4.2.2
This commit is contained in:
@@ -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.2.1
|
||||
VUE_APP_VERSION=v4.2.2
|
||||
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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
VUE_APP_API_HOST=
|
||||
VUE_APP_WS_HOST=
|
||||
VUE_APP_KEY_PREFIX=GeekAI_
|
||||
VUE_APP_VERSION=v4.2.1
|
||||
VUE_APP_VERSION=v4.2.2
|
||||
VUE_APP_TITLE="Geek-AI 创作系统"
|
||||
VUE_APP_DOCS_URL=https://docs.geekai.me
|
||||
VUE_APP_GITHUB_URL=https://github.com/yangjian102621/geekai
|
||||
|
||||
13730
web/package-lock.json
generated
Normal file
13730
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@openai/realtime-api-beta": "github:openai/openai-realtime-api-beta",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^0.27.2",
|
||||
"clipboard": "^2.0.11",
|
||||
@@ -33,12 +32,17 @@
|
||||
"pinia": "^2.1.4",
|
||||
"qrcode": "^1.5.3",
|
||||
"qs": "^6.11.1",
|
||||
"@better-scroll/core": "^2.5.1",
|
||||
"@better-scroll/mouse-wheel": "^2.5.1",
|
||||
"@better-scroll/observe-dom": "^2.5.1",
|
||||
"@better-scroll/pull-up": "^2.5.1",
|
||||
"@better-scroll/scroll-bar": "^2.5.1",
|
||||
"sortablejs": "^1.15.0",
|
||||
"three": "^0.128.0",
|
||||
"v3-waterfall": "^1.3.3",
|
||||
"vant": "^4.5.0",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.15"
|
||||
"vue-router": "^4.0.15",
|
||||
"vue-waterfall-plugin-next": "^2.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.18.6",
|
||||
|
||||
BIN
web/public/images/mj/mj-v7.png
Normal file
BIN
web/public/images/mj/mj-v7.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 840 KiB |
BIN
web/public/images/voice.gif
Normal file
BIN
web/public/images/voice.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -125,6 +125,7 @@
|
||||
//.el-message-box
|
||||
.el-message-box{
|
||||
--el-messagebox-border-radius: 10px
|
||||
--el-messagebox-padding-primary: 24px
|
||||
}
|
||||
.el-message-box__container{
|
||||
//border-top: 1px solid #dbd3f4;
|
||||
|
||||
BIN
web/src/assets/img/loading.gif
Normal file
BIN
web/src/assets/img/loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
@@ -7,7 +7,7 @@
|
||||
|
||||
<div class="chat-item">
|
||||
<div v-if="files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files">
|
||||
<div v-for="file in files" :key="file.url">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
</div>
|
||||
@@ -17,7 +17,9 @@
|
||||
</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>
|
||||
@@ -46,7 +48,7 @@
|
||||
|
||||
<div class="chat-item">
|
||||
<div v-if="files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files">
|
||||
<div v-for="file in files" :key="file.url">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
</div>
|
||||
@@ -56,7 +58,9 @@
|
||||
</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,15 +85,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 { FormatFileSize, GetFileIcon, GetFileType } from '@/store/system'
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { dateFormat, isImage, processPrompt } from '@/utils/libs'
|
||||
import { Clock } from '@element-plus/icons-vue'
|
||||
import hl from 'highlight.js'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import mathjaxPlugin from 'markdown-it-mathjax3'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const md = new MarkdownIt({
|
||||
breaks: true,
|
||||
@@ -97,91 +101,94 @@ 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,
|
||||
"</textarea>"
|
||||
)}</textarea>`;
|
||||
'</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)
|
||||
const urlPrefix = `${window.location.protocol}//${window.location.host}`
|
||||
if (links) {
|
||||
// 把本地链接转换为相对路径
|
||||
const _links = links.map((link) => {
|
||||
if (link.startsWith(urlPrefix)) {
|
||||
return link.replace(urlPrefix, "");
|
||||
return link.replace(urlPrefix, '')
|
||||
}
|
||||
return link;
|
||||
});
|
||||
return link
|
||||
})
|
||||
// 合并数组并去重
|
||||
const urls = [...new Set([...links, ..._links])];
|
||||
httpPost("/api/upload/list", { urls: urls })
|
||||
const urls = [...new Set([...links, ..._links])]
|
||||
httpPost('/api/upload/list', { urls: urls })
|
||||
.then((res) => {
|
||||
files.value = res.data.items;
|
||||
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">
|
||||
@@ -195,7 +202,7 @@ const isExternalImg = (link, files) => {
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 0.5px solid var(--el-border-color);
|
||||
// border-bottom: 0.5px solid var(--el-border-color);
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
@@ -233,6 +240,8 @@ const isExternalImg = (link, files) => {
|
||||
border 1px solid #e3e3e3
|
||||
border-radius 10px
|
||||
margin-bottom 10px
|
||||
max-width 150px
|
||||
max-height 150px
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +375,8 @@ const isExternalImg = (link, files) => {
|
||||
border 1px solid #e3e3e3
|
||||
border-radius 10px
|
||||
margin-bottom 10px
|
||||
max-width 150px
|
||||
max-height 150px
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,111 +1,137 @@
|
||||
<template>
|
||||
<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" />
|
||||
</div>
|
||||
<div class="chat-reply">
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
<div class="chat-item">
|
||||
<div class="content-wrapper" 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-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 class="box-item" effect="dark" content="重新生成" placement="bottom">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
<div class="chat-item">
|
||||
<div class="content-wrapper" v-html="md.render(processContent(data.content))"></div>
|
||||
<div class="bar flex text-gray-500" v-if="data.created_at">
|
||||
<span class="bar-item text-sm">{{ 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>
|
||||
<span v-if="!readOnly" class="flex">
|
||||
<span class="bar-item" @click="reGenerate(data.prompt)">
|
||||
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<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 class="bar-item">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="生成语音朗读"
|
||||
placement="bottom"
|
||||
>
|
||||
<i
|
||||
class="iconfont icon-speaker"
|
||||
v-if="!isPlaying"
|
||||
@click="synthesis(data.content)"
|
||||
></i>
|
||||
<el-image class="voice-icon" :src="playIcon" v-else />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<!-- <span class="bar-item">-->
|
||||
<!-- <el-dropdown trigger="click">-->
|
||||
<!-- <span class="el-dropdown-link">-->
|
||||
<!-- <el-icon><More/></el-icon>-->
|
||||
<!-- </span>-->
|
||||
<!-- <template #dropdown>-->
|
||||
<!-- <el-dropdown-menu>-->
|
||||
<!-- <el-dropdown-item :icon="Headset" @click="synthesis(orgContent)">生成语音</el-dropdown-item>-->
|
||||
<!-- </el-dropdown-menu>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-dropdown>-->
|
||||
<!-- </span>-->
|
||||
<!-- <span class="bar-item">-->
|
||||
<!-- <el-dropdown trigger="click">-->
|
||||
<!-- <span class="el-dropdown-link">-->
|
||||
<!-- <el-icon><More/></el-icon>-->
|
||||
<!-- </span>-->
|
||||
<!-- <template #dropdown>-->
|
||||
<!-- <el-dropdown-menu>-->
|
||||
<!-- <el-dropdown-item :icon="Headset" @click="synthesis(orgContent)">生成语音</el-dropdown-item>-->
|
||||
<!-- </el-dropdown-menu>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-dropdown>-->
|
||||
<!-- </span>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
</div>
|
||||
<div class="chat-item">
|
||||
<div class="content-wrapper">
|
||||
<div class="content" v-html="md.render(processContent(data.content))"></div>
|
||||
<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" />
|
||||
</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 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>
|
||||
<span v-if="!readOnly">
|
||||
<span class="bar-item bg" @click="reGenerate(data.prompt)">
|
||||
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
<div class="chat-item">
|
||||
<div class="content-wrapper">
|
||||
<div class="content" v-html="md.render(processContent(data.content))"></div>
|
||||
</div>
|
||||
<div class="bar text-gray-500" v-if="data.created_at">
|
||||
<span class="bar-item text-sm"> {{ 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>
|
||||
<span v-if="!readOnly" class="flex">
|
||||
<span class="bar-item bg" @click="reGenerate(data.prompt)">
|
||||
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
|
||||
<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 class="bar-item bg">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="生成语音朗读"
|
||||
placement="bottom"
|
||||
v-if="!isPlaying"
|
||||
>
|
||||
<i class="iconfont icon-speaker" @click="synthesis(data.content)"></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
effect="dark"
|
||||
content="暂停播放"
|
||||
placement="bottom"
|
||||
v-else
|
||||
>
|
||||
<el-image class="voice-icon" :src="playIcon" @click="stopSynthesis()" />
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<audio ref="audio" @ended="isPlaying = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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";
|
||||
import emoji from "markdown-it-emoji";
|
||||
import mathjaxPlugin from "markdown-it-mathjax3";
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { dateFormat, processContent } from '@/utils/libs'
|
||||
import { DocumentCopy, Refresh } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import hl from 'highlight.js'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import emoji from 'markdown-it-emoji'
|
||||
import mathjaxPlugin from 'markdown-it-mathjax3'
|
||||
import { ref } from 'vue'
|
||||
// eslint-disable-next-line no-undef,no-unused-vars
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: {
|
||||
icon: "",
|
||||
content: "",
|
||||
created_at: "",
|
||||
icon: '',
|
||||
content: '',
|
||||
created_at: '',
|
||||
tokens: 0,
|
||||
},
|
||||
},
|
||||
@@ -115,9 +141,14 @@ const props = defineProps({
|
||||
},
|
||||
listStyle: {
|
||||
type: String,
|
||||
default: "list",
|
||||
default: 'list',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const audio = ref(null)
|
||||
const isPlaying = ref(false)
|
||||
const playIcon = ref('/images/voice.gif')
|
||||
const store = useSharedStore()
|
||||
|
||||
const md = new MarkdownIt({
|
||||
breaks: true,
|
||||
@@ -125,49 +156,77 @@ 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,
|
||||
"</textarea>"
|
||||
)}</textarea>`;
|
||||
'</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(str, { language: lang }).value;
|
||||
const preCode = hl.highlight(str, { language: lang }).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);
|
||||
const emits = defineEmits(["regen"]);
|
||||
})
|
||||
md.use(mathjaxPlugin)
|
||||
md.use(emoji)
|
||||
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("语音合成功能暂不可用");
|
||||
};
|
||||
isPlaying.value = true
|
||||
httpPost('/api/chat/tts', { text: text, model_id: store.ttsModel }, { responseType: 'blob' })
|
||||
.then((response) => {
|
||||
// 创建 Blob 对象,明确指定 MIME 类型
|
||||
const blob = new Blob([response], { type: 'audio/mpeg' }) // 假设音频格式为 MP3
|
||||
const audioUrl = URL.createObjectURL(blob)
|
||||
// 播放音频
|
||||
audio.value.src = audioUrl
|
||||
audio.value
|
||||
.play()
|
||||
.then(() => {
|
||||
// 播放完成后释放 URL
|
||||
URL.revokeObjectURL(audioUrl)
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error('音频播放失败,请检查浏览器是否支持该音频格式')
|
||||
isPlaying.value = false
|
||||
})
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('语音合成失败:' + e.message)
|
||||
isPlaying.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const stopSynthesis = () => {
|
||||
isPlaying.value = false
|
||||
audio.value.pause()
|
||||
audio.value.currentTime = 0
|
||||
}
|
||||
|
||||
// 重新生成
|
||||
const reGenerate = (prompt) => {
|
||||
console.log(prompt);
|
||||
emits("regen", prompt);
|
||||
};
|
||||
console.log(prompt)
|
||||
emits('regen', prompt)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import '@/assets/css/markdown/vue.css';
|
||||
|
||||
.chat-page,.chat-export {
|
||||
--font-family: Menlo,"微软雅黑","Roboto Mono","Courier New",Courier,monospace,"Inter",sans-serif;
|
||||
font-family: var(--font-family);
|
||||
@@ -265,7 +324,7 @@ const reGenerate = (prompt) => {
|
||||
// 代码快
|
||||
|
||||
blockquote {
|
||||
margin 0
|
||||
margin 0 0 0.8rem 0
|
||||
background-color: var(--quote-bg-color);
|
||||
padding: 0.8rem 1.5rem;
|
||||
color: var(--quote-text-color);
|
||||
@@ -284,7 +343,8 @@ const reGenerate = (prompt) => {
|
||||
width 100%
|
||||
padding-bottom: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-bottom: 0.5px solid var(--el-border-color);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 10px;
|
||||
|
||||
.chat-line-inner {
|
||||
display flex;
|
||||
@@ -324,10 +384,18 @@ const reGenerate = (prompt) => {
|
||||
padding 10px 10px 10px 0;
|
||||
|
||||
.bar-item {
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
cursor pointer
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
height 26px
|
||||
|
||||
.voice-icon {
|
||||
width 20px
|
||||
height 20px
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
@@ -403,11 +471,21 @@ const reGenerate = (prompt) => {
|
||||
|
||||
.bar {
|
||||
padding 10px 10px 10px 0;
|
||||
display flex
|
||||
|
||||
.bar-item {
|
||||
padding 3px 5px;
|
||||
margin-right 10px;
|
||||
border-radius 5px;
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
height 26px
|
||||
|
||||
.voice-icon {
|
||||
width 20px
|
||||
height 20px
|
||||
}
|
||||
|
||||
|
||||
.el-icon {
|
||||
position relative
|
||||
|
||||
@@ -18,19 +18,28 @@
|
||||
<el-form-item label="流式输出:">
|
||||
<el-switch v-model="data.stream" @change="(val) => {store.setChatStream(val)}" />
|
||||
</el-form-item>
|
||||
<el-form-item label="语音音色:">
|
||||
<el-select v-model="data.ttsModel" placeholder="请选择语音音色" @change="changeTTSModel">
|
||||
<el-option v-for="v in models" :value="v.id" :label="v.name" :key="v.id">
|
||||
{{ v.name }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, ref} from "vue"
|
||||
import {computed, ref, onMounted} from "vue"
|
||||
import {useSharedStore} from "@/store/sharedata";
|
||||
import {httpGet} from "@/utils/http";
|
||||
const store = useSharedStore();
|
||||
|
||||
const data = ref({
|
||||
style: store.chatListStyle,
|
||||
stream: store.chatStream,
|
||||
ttsModel: store.ttsModel,
|
||||
})
|
||||
// eslint-disable-next-line no-undef
|
||||
const props = defineProps({
|
||||
@@ -44,6 +53,20 @@ const emits = defineEmits(['hide']);
|
||||
const close = function () {
|
||||
emits('hide', false);
|
||||
}
|
||||
const models = ref([]);
|
||||
onMounted(() => {
|
||||
// 获取模型列表
|
||||
httpGet("/api/model/list?type=tts").then((res) => {
|
||||
models.value = res.data;
|
||||
if (!data.ttsModel) {
|
||||
store.setTtsModel(models.value[0].id);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const changeTTSModel = (item) => {
|
||||
store.setTtsModel(item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
@@ -25,7 +25,11 @@
|
||||
<template v-for="subItem in item.subs">
|
||||
<el-sub-menu v-if="subItem.subs" :index="subItem.index" :key="subItem.index">
|
||||
<template #title>{{ subItem.title }}</template>
|
||||
<el-menu-item v-for="(threeItem, i) in subItem.subs" :key="i" :index="threeItem.index">
|
||||
<el-menu-item
|
||||
v-for="(threeItem, i) in subItem.subs"
|
||||
:key="i"
|
||||
:index="threeItem.index"
|
||||
>
|
||||
{{ threeItem.title }}
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
@@ -48,125 +52,135 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { setMenuItems, useSidebarStore } from "@/store/sidebar";
|
||||
import { httpGet } from "@/utils/http";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { setMenuItems, useSidebarStore } from '@/store/sidebar'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const title = ref("");
|
||||
const logo = ref("");
|
||||
const title = ref('')
|
||||
const logo = ref('')
|
||||
|
||||
// 加载系统配置
|
||||
httpGet("/api/admin/config/get?key=system")
|
||||
httpGet('/api/admin/config/get?key=system')
|
||||
.then((res) => {
|
||||
title.value = res.data.admin_title;
|
||||
logo.value = res.data.logo;
|
||||
title.value = res.data.admin_title
|
||||
logo.value = res.data.logo
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("加载系统配置失败: " + e.message);
|
||||
});
|
||||
const store = useSharedStore();
|
||||
const theme = ref(store.theme);
|
||||
ElMessage.error('加载系统配置失败: ' + e.message)
|
||||
})
|
||||
const store = useSharedStore()
|
||||
const theme = ref(store.theme)
|
||||
watch(
|
||||
() => store.theme,
|
||||
(val) => {
|
||||
theme.value = val;
|
||||
theme.value = val
|
||||
}
|
||||
);
|
||||
)
|
||||
const items = [
|
||||
{
|
||||
icon: "home",
|
||||
index: "/admin/dashboard",
|
||||
title: "仪表盘",
|
||||
icon: 'home',
|
||||
index: '/admin/dashboard',
|
||||
title: '仪表盘',
|
||||
},
|
||||
|
||||
{
|
||||
icon: "user-fill",
|
||||
index: "/admin/user",
|
||||
title: "用户管理",
|
||||
icon: 'user-fill',
|
||||
index: '/admin/user',
|
||||
title: '用户管理',
|
||||
},
|
||||
{
|
||||
icon: "menu",
|
||||
index: "1",
|
||||
title: "应用管理",
|
||||
icon: 'menu',
|
||||
index: '1',
|
||||
title: '应用管理',
|
||||
subs: [
|
||||
{
|
||||
index: "/admin/app",
|
||||
title: "应用列表",
|
||||
index: '/admin/app',
|
||||
title: '应用列表',
|
||||
icon: 'sub-menu',
|
||||
},
|
||||
{
|
||||
index: "/admin/app/type",
|
||||
title: "应用分类",
|
||||
index: '/admin/app/type',
|
||||
title: '应用分类',
|
||||
icon: 'chuangzuo',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
icon: "api-key",
|
||||
index: "/admin/apikey",
|
||||
title: "API-KEY",
|
||||
icon: 'api-key',
|
||||
index: '/admin/apikey',
|
||||
title: 'API-KEY',
|
||||
},
|
||||
{
|
||||
icon: "model",
|
||||
index: "/admin/chat/model",
|
||||
title: "模型管理",
|
||||
icon: 'model',
|
||||
index: '/admin/chat/model',
|
||||
title: '模型管理',
|
||||
},
|
||||
{
|
||||
icon: "recharge",
|
||||
index: "/admin/product",
|
||||
title: "充值产品",
|
||||
icon: 'recharge',
|
||||
index: '/admin/product',
|
||||
title: '充值产品',
|
||||
},
|
||||
{
|
||||
icon: "order",
|
||||
index: "/admin/order",
|
||||
title: "充值订单",
|
||||
icon: 'order',
|
||||
index: '/admin/order',
|
||||
title: '充值订单',
|
||||
},
|
||||
{
|
||||
icon: "reward",
|
||||
index: "/admin/redeem",
|
||||
title: "兑换码",
|
||||
icon: 'reward',
|
||||
index: '/admin/redeem',
|
||||
title: '兑换码',
|
||||
},
|
||||
{
|
||||
icon: "control",
|
||||
index: "/admin/functions",
|
||||
title: "函数管理",
|
||||
icon: 'control',
|
||||
index: '/admin/functions',
|
||||
title: '函数管理',
|
||||
},
|
||||
{
|
||||
icon: "prompt",
|
||||
index: "/admin/chats",
|
||||
title: "对话管理",
|
||||
icon: 'menu',
|
||||
index: '2',
|
||||
title: '创作记录',
|
||||
subs: [
|
||||
{
|
||||
icon: 'prompt',
|
||||
index: '/admin/chats',
|
||||
title: '对话记录',
|
||||
},
|
||||
{
|
||||
icon: 'image',
|
||||
index: '/admin/images',
|
||||
title: '绘图记录',
|
||||
},
|
||||
{
|
||||
icon: 'mp3',
|
||||
index: '/admin/medias',
|
||||
title: '音视频记录',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
icon: 'role',
|
||||
index: '/admin/manger',
|
||||
title: '管理员',
|
||||
},
|
||||
{
|
||||
icon: "image",
|
||||
index: "/admin/images",
|
||||
title: "绘图管理",
|
||||
icon: 'config',
|
||||
index: '/admin/system',
|
||||
title: '系统设置',
|
||||
},
|
||||
{
|
||||
icon: "mp3",
|
||||
index: "/admin/medias",
|
||||
title: "音视频管理",
|
||||
icon: 'log',
|
||||
index: '/admin/powerLog',
|
||||
title: '用户算力日志',
|
||||
},
|
||||
{
|
||||
icon: "role",
|
||||
index: "/admin/manger",
|
||||
title: "管理员",
|
||||
},
|
||||
{
|
||||
icon: "config",
|
||||
index: "/admin/system",
|
||||
title: "系统设置",
|
||||
},
|
||||
{
|
||||
icon: "log",
|
||||
index: "/admin/powerLog",
|
||||
title: "用户算力日志",
|
||||
},
|
||||
{
|
||||
icon: "log",
|
||||
index: "/admin/loginLog",
|
||||
title: "用户登录日志",
|
||||
icon: 'log',
|
||||
index: '/admin/loginLog',
|
||||
title: '用户登录日志',
|
||||
},
|
||||
// {
|
||||
// icon: 'menu',
|
||||
@@ -191,15 +205,15 @@ const items = [
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
];
|
||||
]
|
||||
|
||||
const route = useRoute();
|
||||
const route = useRoute()
|
||||
const onRoutes = computed(() => {
|
||||
return route.path;
|
||||
});
|
||||
return route.path
|
||||
})
|
||||
|
||||
const sidebar = useSidebarStore();
|
||||
setMenuItems(items);
|
||||
const sidebar = useSidebarStore()
|
||||
setMenuItems(items)
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
|
||||
@@ -64,8 +64,6 @@ import {
|
||||
Uploader,
|
||||
} from "vant";
|
||||
import { router } from "@/router";
|
||||
import "v3-waterfall/dist/style.css";
|
||||
import V3waterfall from "v3-waterfall";
|
||||
import "@/assets/css/theme-dark.styl";
|
||||
import "@/assets/css/theme-light.styl";
|
||||
import "@/assets/css/common.styl";
|
||||
@@ -104,7 +102,6 @@ app.use(ShareSheet);
|
||||
app.use(Switch);
|
||||
app.use(Uploader);
|
||||
app.use(Tag);
|
||||
app.use(V3waterfall);
|
||||
app.use(Overlay);
|
||||
app.use(Col);
|
||||
app.use(Row);
|
||||
|
||||
@@ -5,357 +5,357 @@
|
||||
// * @Author yangjian102621@163.com
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import {createRouter, createWebHistory} from "vue-router";
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
name: "Index",
|
||||
path: "/",
|
||||
meta: { title: "首页" },
|
||||
component: () => import("@/views/Index.vue"),
|
||||
name: 'Index',
|
||||
path: '/',
|
||||
meta: { title: '首页' },
|
||||
component: () => import('@/views/Index.vue'),
|
||||
},
|
||||
{
|
||||
name: "home",
|
||||
path: "/home",
|
||||
redirect: "/chat",
|
||||
component: () => import("@/views/Home.vue"),
|
||||
name: 'home',
|
||||
path: '/home',
|
||||
redirect: '/chat',
|
||||
component: () => import('@/views/Home.vue'),
|
||||
children: [
|
||||
{
|
||||
name: "chat",
|
||||
path: "/chat",
|
||||
meta: { title: "创作中心" },
|
||||
component: () => import("@/views/ChatPlus.vue"),
|
||||
name: 'chat',
|
||||
path: '/chat',
|
||||
meta: { title: '创作中心' },
|
||||
component: () => import('@/views/ChatPlus.vue'),
|
||||
},
|
||||
{
|
||||
name: "chat-id",
|
||||
path: "/chat/:id",
|
||||
meta: { title: "创作中心" },
|
||||
component: () => import("@/views/ChatPlus.vue"),
|
||||
name: 'chat-id',
|
||||
path: '/chat/:id',
|
||||
meta: { title: '创作中心' },
|
||||
component: () => import('@/views/ChatPlus.vue'),
|
||||
},
|
||||
{
|
||||
name: "image-mj",
|
||||
path: "/mj",
|
||||
meta: { title: "MidJourney 绘画中心" },
|
||||
component: () => import("@/views/ImageMj.vue"),
|
||||
name: 'image-mj',
|
||||
path: '/mj',
|
||||
meta: { title: 'MidJourney 绘画中心' },
|
||||
component: () => import('@/views/ImageMj.vue'),
|
||||
},
|
||||
{
|
||||
name: "image-sd",
|
||||
path: "/sd",
|
||||
meta: { title: "stable diffusion 绘画中心" },
|
||||
component: () => import("@/views/ImageSd.vue"),
|
||||
name: 'image-sd',
|
||||
path: '/sd',
|
||||
meta: { title: 'stable diffusion 绘画中心' },
|
||||
component: () => import('@/views/ImageSd.vue'),
|
||||
},
|
||||
{
|
||||
name: "member",
|
||||
path: "/member",
|
||||
meta: { title: "会员充值中心" },
|
||||
component: () => import("@/views/Member.vue"),
|
||||
name: 'member',
|
||||
path: '/member',
|
||||
meta: { title: '会员充值中心' },
|
||||
component: () => import('@/views/Member.vue'),
|
||||
},
|
||||
{
|
||||
name: "chat-app",
|
||||
path: "/apps",
|
||||
meta: { title: "应用中心" },
|
||||
component: () => import("@/views/ChatApps.vue"),
|
||||
name: 'chat-app',
|
||||
path: '/apps',
|
||||
meta: { title: '应用中心' },
|
||||
component: () => import('@/views/ChatApps.vue'),
|
||||
},
|
||||
{
|
||||
name: "images",
|
||||
path: "/images-wall",
|
||||
meta: { title: "作品展示" },
|
||||
component: () => import("@/views/ImagesWall.vue"),
|
||||
name: 'images',
|
||||
path: '/images-wall',
|
||||
meta: { title: '作品展示' },
|
||||
component: () => import('@/views/ImagesWall.vue'),
|
||||
},
|
||||
{
|
||||
name: "user-invitation",
|
||||
path: "/invite",
|
||||
meta: { title: "推广计划" },
|
||||
component: () => import("@/views/Invitation.vue"),
|
||||
name: 'user-invitation',
|
||||
path: '/invite',
|
||||
meta: { title: '推广计划' },
|
||||
component: () => import('@/views/Invitation.vue'),
|
||||
},
|
||||
{
|
||||
name: "powerLog",
|
||||
path: "/powerLog",
|
||||
meta: { title: "消费日志" },
|
||||
component: () => import("@/views/PowerLog.vue"),
|
||||
name: 'powerLog',
|
||||
path: '/powerLog',
|
||||
meta: { title: '消费日志' },
|
||||
component: () => import('@/views/PowerLog.vue'),
|
||||
},
|
||||
{
|
||||
name: "xmind",
|
||||
path: "/xmind",
|
||||
meta: { title: "思维导图" },
|
||||
component: () => import("@/views/MarkMap.vue"),
|
||||
name: 'xmind',
|
||||
path: '/xmind',
|
||||
meta: { title: '思维导图' },
|
||||
component: () => import('@/views/MarkMap.vue'),
|
||||
},
|
||||
{
|
||||
name: "dalle",
|
||||
path: "/dalle",
|
||||
meta: { title: "DALLE-3" },
|
||||
component: () => import("@/views/Dalle.vue"),
|
||||
name: 'dalle',
|
||||
path: '/dalle',
|
||||
meta: { title: 'DALLE-3' },
|
||||
component: () => import('@/views/Dalle.vue'),
|
||||
},
|
||||
{
|
||||
name: "suno",
|
||||
path: "/suno",
|
||||
meta: { title: "Suno音乐创作" },
|
||||
component: () => import("@/views/Suno.vue"),
|
||||
name: 'suno',
|
||||
path: '/suno',
|
||||
meta: { title: 'Suno音乐创作' },
|
||||
component: () => import('@/views/Suno.vue'),
|
||||
},
|
||||
{
|
||||
name: "ExternalLink",
|
||||
path: "/external",
|
||||
component: () => import("@/views/ExternalPage.vue"),
|
||||
name: 'ExternalLink',
|
||||
path: '/external',
|
||||
component: () => import('@/views/ExternalPage.vue'),
|
||||
},
|
||||
{
|
||||
name: "song",
|
||||
path: "/song/:id",
|
||||
meta: { title: "Suno音乐播放" },
|
||||
component: () => import("@/views/Song.vue"),
|
||||
name: 'song',
|
||||
path: '/song/:id',
|
||||
meta: { title: 'Suno音乐播放' },
|
||||
component: () => import('@/views/Song.vue'),
|
||||
},
|
||||
{
|
||||
name: "luma",
|
||||
path: "/luma",
|
||||
meta: { title: "Luma视频创作" },
|
||||
component: () => import("@/views/Luma.vue"),
|
||||
name: 'luma',
|
||||
path: '/luma',
|
||||
meta: { title: 'Luma视频创作' },
|
||||
component: () => import('@/views/Luma.vue'),
|
||||
},
|
||||
{
|
||||
name: "keling",
|
||||
path: "/keling",
|
||||
meta: { title: "KeLing视频创作" },
|
||||
component: () => import("@/views/KeLing.vue"),
|
||||
name: 'keling',
|
||||
path: '/keling',
|
||||
meta: { title: 'KeLing视频创作' },
|
||||
component: () => import('@/views/KeLing.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "chat-export",
|
||||
path: "/chat/export",
|
||||
meta: { title: "导出会话记录" },
|
||||
component: () => import("@/views/ChatExport.vue"),
|
||||
name: 'chat-export',
|
||||
path: '/chat/export',
|
||||
meta: { title: '导出会话记录' },
|
||||
component: () => import('@/views/ChatExport.vue'),
|
||||
},
|
||||
{
|
||||
name: "login",
|
||||
path: "/login",
|
||||
meta: { title: "用户登录" },
|
||||
component: () => import("@/views/Login.vue"),
|
||||
name: 'login',
|
||||
path: '/login',
|
||||
meta: { title: '用户登录' },
|
||||
component: () => import('@/views/Login.vue'),
|
||||
},
|
||||
|
||||
{
|
||||
name: "login-callback",
|
||||
path: "/login/callback",
|
||||
meta: { title: "用户登录" },
|
||||
component: () => import("@/views/LoginCallback.vue"),
|
||||
name: 'login-callback',
|
||||
path: '/login/callback',
|
||||
meta: { title: '用户登录' },
|
||||
component: () => import('@/views/LoginCallback.vue'),
|
||||
},
|
||||
{
|
||||
name: "register",
|
||||
path: "/register",
|
||||
name: 'register',
|
||||
path: '/register',
|
||||
|
||||
meta: { title: "用户注册" },
|
||||
component: () => import("@/views/Register.vue"),
|
||||
meta: { title: '用户注册' },
|
||||
component: () => import('@/views/Register.vue'),
|
||||
},
|
||||
{
|
||||
name: "resetpassword",
|
||||
path: "/resetpassword",
|
||||
meta: { title: "重置密码" },
|
||||
component: () => import("@/views/Resetpassword.vue"),
|
||||
name: 'resetpassword',
|
||||
path: '/resetpassword',
|
||||
meta: { title: '重置密码' },
|
||||
component: () => import('@/views/Resetpassword.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/login",
|
||||
name: "admin-login",
|
||||
meta: { title: "控制台登录" },
|
||||
component: () => import("@/views/admin/Login.vue"),
|
||||
path: '/admin/login',
|
||||
name: 'admin-login',
|
||||
meta: { title: '控制台登录' },
|
||||
component: () => import('@/views/admin/Login.vue'),
|
||||
},
|
||||
{
|
||||
path: "/payReturn",
|
||||
name: "pay-return",
|
||||
meta: { title: "支付回调" },
|
||||
component: () => import("@/views/PayReturn.vue"),
|
||||
path: '/payReturn',
|
||||
name: 'pay-return',
|
||||
meta: { title: '支付回调' },
|
||||
component: () => import('@/views/PayReturn.vue'),
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
path: "/admin",
|
||||
redirect: "/admin/dashboard",
|
||||
component: () => import("@/views/admin/Home.vue"),
|
||||
meta: { title: "Geek-AI 控制台" },
|
||||
name: 'admin',
|
||||
path: '/admin',
|
||||
redirect: '/admin/dashboard',
|
||||
component: () => import('@/views/admin/Home.vue'),
|
||||
meta: { title: 'Geek-AI 控制台' },
|
||||
children: [
|
||||
{
|
||||
path: "/admin/dashboard",
|
||||
name: "admin-dashboard",
|
||||
meta: { title: "仪表盘" },
|
||||
component: () => import("@/views/admin/Dashboard.vue"),
|
||||
path: '/admin/dashboard',
|
||||
name: 'admin-dashboard',
|
||||
meta: { title: '仪表盘' },
|
||||
component: () => import('@/views/admin/Dashboard.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/system",
|
||||
name: "admin-system",
|
||||
meta: { title: "系统设置" },
|
||||
component: () => import("@/views/admin/SysConfig.vue"),
|
||||
path: '/admin/system',
|
||||
name: 'admin-system',
|
||||
meta: { title: '系统设置' },
|
||||
component: () => import('@/views/admin/SysConfig.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/user",
|
||||
name: "admin-user",
|
||||
meta: { title: "用户管理" },
|
||||
component: () => import("@/views/admin/Users.vue"),
|
||||
path: '/admin/user',
|
||||
name: 'admin-user',
|
||||
meta: { title: '用户管理' },
|
||||
component: () => import('@/views/admin/Users.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/app",
|
||||
name: "admin-app",
|
||||
meta: { title: "应用列表" },
|
||||
component: () => import("@/views/admin/Apps.vue"),
|
||||
path: '/admin/app',
|
||||
name: 'admin-app',
|
||||
meta: { title: '应用列表' },
|
||||
component: () => import('@/views/admin/Apps.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/app/type",
|
||||
name: "admin-app-type",
|
||||
meta: { title: "应用分类" },
|
||||
component: () => import("@/views/admin/AppType.vue"),
|
||||
path: '/admin/app/type',
|
||||
name: 'admin-app-type',
|
||||
meta: { title: '应用分类' },
|
||||
component: () => import('@/views/admin/AppType.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/apikey",
|
||||
name: "admin-apikey",
|
||||
meta: { title: "API-KEY 管理" },
|
||||
component: () => import("@/views/admin/ApiKey.vue"),
|
||||
path: '/admin/apikey',
|
||||
name: 'admin-apikey',
|
||||
meta: { title: 'API-KEY 管理' },
|
||||
component: () => import('@/views/admin/ApiKey.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/chat/model",
|
||||
name: "admin-chat-model",
|
||||
meta: { title: "语言模型" },
|
||||
component: () => import("@/views/admin/ChatModel.vue"),
|
||||
path: '/admin/chat/model',
|
||||
name: 'admin-chat-model',
|
||||
meta: { title: '语言模型' },
|
||||
component: () => import('@/views/admin/ChatModel.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/product",
|
||||
name: "admin-product",
|
||||
meta: { title: "充值产品" },
|
||||
component: () => import("@/views/admin/Product.vue"),
|
||||
path: '/admin/product',
|
||||
name: 'admin-product',
|
||||
meta: { title: '充值产品' },
|
||||
component: () => import('@/views/admin/Product.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/order",
|
||||
name: "admin-order",
|
||||
meta: { title: "充值订单" },
|
||||
component: () => import("@/views/admin/Order.vue"),
|
||||
path: '/admin/order',
|
||||
name: 'admin-order',
|
||||
meta: { title: '充值订单' },
|
||||
component: () => import('@/views/admin/Order.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/redeem",
|
||||
name: "admin-redeem",
|
||||
meta: { title: "兑换码管理" },
|
||||
component: () => import("@/views/admin/Redeem.vue"),
|
||||
path: '/admin/redeem',
|
||||
name: 'admin-redeem',
|
||||
meta: { title: '兑换码管理' },
|
||||
component: () => import('@/views/admin/Redeem.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/loginLog",
|
||||
name: "admin-loginLog",
|
||||
meta: { title: "登录日志" },
|
||||
component: () => import("@/views/admin/LoginLog.vue"),
|
||||
path: '/admin/loginLog',
|
||||
name: 'admin-loginLog',
|
||||
meta: { title: '登录日志' },
|
||||
component: () => import('@/views/admin/LoginLog.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/functions",
|
||||
name: "admin-functions",
|
||||
meta: { title: "函数管理" },
|
||||
component: () => import("@/views/admin/Functions.vue"),
|
||||
path: '/admin/functions',
|
||||
name: 'admin-functions',
|
||||
meta: { title: '函数管理' },
|
||||
component: () => import('@/views/admin/Functions.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/chats",
|
||||
name: "admin-chats",
|
||||
meta: { title: "对话管理" },
|
||||
component: () => import("@/views/admin/ChatList.vue"),
|
||||
path: '/admin/chats',
|
||||
name: 'admin-chats',
|
||||
meta: { title: '对话管理' },
|
||||
component: () => import('@/views/admin/records/ChatList.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/images",
|
||||
name: "admin-images",
|
||||
meta: { title: "绘图管理" },
|
||||
component: () => import("@/views/admin/ImageList.vue"),
|
||||
path: '/admin/images',
|
||||
name: 'admin-images',
|
||||
meta: { title: '绘图管理' },
|
||||
component: () => import('@/views/admin/records/ImageList.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/medias",
|
||||
name: "admin-medias",
|
||||
meta: { title: "音视频管理" },
|
||||
component: () => import("@/views/admin/Medias.vue"),
|
||||
path: '/admin/medias',
|
||||
name: 'admin-medias',
|
||||
meta: { title: '音视频管理' },
|
||||
component: () => import('@/views/admin/records/Medias.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/powerLog",
|
||||
name: "admin-power-log",
|
||||
meta: { title: "算力日志" },
|
||||
component: () => import("@/views/admin/PowerLog.vue"),
|
||||
path: '/admin/powerLog',
|
||||
name: 'admin-power-log',
|
||||
meta: { title: '算力日志' },
|
||||
component: () => import('@/views/admin/PowerLog.vue'),
|
||||
},
|
||||
{
|
||||
path: "/admin/manger",
|
||||
name: "admin-manger",
|
||||
meta: { title: "管理员" },
|
||||
component: () => import("@/views/admin/Manager.vue"),
|
||||
path: '/admin/manger',
|
||||
name: 'admin-manger',
|
||||
meta: { title: '管理员' },
|
||||
component: () => import('@/views/admin/Manager.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "mobile-login",
|
||||
path: "/mobile/login",
|
||||
meta: { title: "用户登录" },
|
||||
component: () => import("@/views/mobile/Login.vue"),
|
||||
name: 'mobile-login',
|
||||
path: '/mobile/login',
|
||||
meta: { title: '用户登录' },
|
||||
component: () => import('@/views/mobile/Login.vue'),
|
||||
},
|
||||
{
|
||||
name: "mobile",
|
||||
path: "/mobile",
|
||||
meta: { title: "首页" },
|
||||
component: () => import("@/views/mobile/Home.vue"),
|
||||
redirect: "/mobile/index",
|
||||
name: 'mobile',
|
||||
path: '/mobile',
|
||||
meta: { title: '首页' },
|
||||
component: () => import('@/views/mobile/Home.vue'),
|
||||
redirect: '/mobile/index',
|
||||
children: [
|
||||
{
|
||||
path: "/mobile/index",
|
||||
name: "mobile-index",
|
||||
component: () => import("@/views/mobile/Index.vue"),
|
||||
path: '/mobile/index',
|
||||
name: 'mobile-index',
|
||||
component: () => import('@/views/mobile/Index.vue'),
|
||||
},
|
||||
{
|
||||
path: "/mobile/chat",
|
||||
name: "mobile-chat",
|
||||
component: () => import("@/views/mobile/ChatList.vue"),
|
||||
path: '/mobile/chat',
|
||||
name: 'mobile-chat',
|
||||
component: () => import('@/views/mobile/ChatList.vue'),
|
||||
},
|
||||
{
|
||||
path: "/mobile/image",
|
||||
name: "mobile-image",
|
||||
component: () => import("@/views/mobile/Image.vue"),
|
||||
path: '/mobile/image',
|
||||
name: 'mobile-image',
|
||||
component: () => import('@/views/mobile/Image.vue'),
|
||||
},
|
||||
{
|
||||
path: "/mobile/profile",
|
||||
name: "mobile-profile",
|
||||
component: () => import("@/views/mobile/Profile.vue"),
|
||||
path: '/mobile/profile',
|
||||
name: 'mobile-profile',
|
||||
component: () => import('@/views/mobile/Profile.vue'),
|
||||
},
|
||||
{
|
||||
path: "/mobile/imgWall",
|
||||
name: "mobile-img-wall",
|
||||
component: () => import("@/views/mobile/pages/ImgWall.vue"),
|
||||
path: '/mobile/imgWall',
|
||||
name: 'mobile-img-wall',
|
||||
component: () => import('@/views/mobile/pages/ImgWall.vue'),
|
||||
},
|
||||
{
|
||||
path: "/mobile/chat/session",
|
||||
name: "mobile-chat-session",
|
||||
component: () => import("@/views/mobile/ChatSession.vue"),
|
||||
path: '/mobile/chat/session',
|
||||
name: 'mobile-chat-session',
|
||||
component: () => import('@/views/mobile/ChatSession.vue'),
|
||||
},
|
||||
{
|
||||
path: "/mobile/chat/export",
|
||||
name: "mobile-chat-export",
|
||||
component: () => import("@/views/mobile/ChatExport.vue"),
|
||||
path: '/mobile/chat/export',
|
||||
name: 'mobile-chat-export',
|
||||
component: () => import('@/views/mobile/ChatExport.vue'),
|
||||
},
|
||||
{
|
||||
path: '/mobile/apps',
|
||||
name: 'mobile-apps',
|
||||
component: () => import('@/views/mobile/Apps.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "test",
|
||||
path: "/test",
|
||||
meta: { title: "测试页面" },
|
||||
component: () => import("@/views/Test.vue"),
|
||||
name: 'test',
|
||||
path: '/test',
|
||||
meta: { title: '测试页面' },
|
||||
component: () => import('@/views/Test.vue'),
|
||||
},
|
||||
|
||||
{
|
||||
name: "test2",
|
||||
path: "/test2",
|
||||
meta: { title: "测试页面" },
|
||||
component: () => import("@/views/RealtimeTest.vue"),
|
||||
name: 'NotFound',
|
||||
path: '/:all(.*)',
|
||||
meta: { title: '页面没有找到' },
|
||||
component: () => import('@/views/404.vue'),
|
||||
},
|
||||
{
|
||||
name: "NotFound",
|
||||
path: "/:all(.*)",
|
||||
meta: { title: "页面没有找到" },
|
||||
component: () => import("@/views/404.vue"),
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
// console.log(MY_VARIABLE)
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: routes,
|
||||
});
|
||||
})
|
||||
|
||||
let prevRoute = null;
|
||||
let prevRoute = null
|
||||
// dynamic change the title when router change
|
||||
router.beforeEach((to, from, next) => {
|
||||
document.title = to.meta.title;
|
||||
prevRoute = from;
|
||||
next();
|
||||
});
|
||||
document.title = to.meta.title
|
||||
prevRoute = from
|
||||
next()
|
||||
})
|
||||
|
||||
export { router, prevRoute };
|
||||
export { router, prevRoute }
|
||||
|
||||
@@ -1,5 +1,82 @@
|
||||
import {defineStore} from "pinia";
|
||||
import Storage from "good-storage";
|
||||
import errorIcon from "@/assets/img/failed.png";
|
||||
import loadingIcon from "@/assets/img/loading.gif";
|
||||
|
||||
let waterfallOptions = {
|
||||
// 唯一key值
|
||||
rowKey: "id",
|
||||
// 卡片之间的间隙
|
||||
gutter: 10,
|
||||
// 是否有周围的gutter
|
||||
hasAroundGutter: true,
|
||||
// 卡片在PC上的宽度
|
||||
width: 200,
|
||||
// 自定义行显示个数,主要用于对移动端的适配
|
||||
breakpoints: {
|
||||
3840: {
|
||||
// 4K下
|
||||
rowPerView: 8,
|
||||
},
|
||||
2560: {
|
||||
// 2K下
|
||||
rowPerView: 7,
|
||||
},
|
||||
1920: {
|
||||
// 2K下
|
||||
rowPerView: 6,
|
||||
},
|
||||
1600: {
|
||||
// 2K下
|
||||
rowPerView: 5,
|
||||
},
|
||||
1366: {
|
||||
// 2K下
|
||||
rowPerView: 4,
|
||||
},
|
||||
800: {
|
||||
// 当屏幕宽度小于等于800
|
||||
rowPerView: 3,
|
||||
},
|
||||
500: {
|
||||
// 当屏幕宽度小于等于500
|
||||
rowPerView: 2,
|
||||
},
|
||||
},
|
||||
// 动画效果
|
||||
animationEffect: "animate__fadeInUp",
|
||||
// 动画时间
|
||||
animationDuration: 1000,
|
||||
// 动画延迟
|
||||
animationDelay: 300,
|
||||
animationCancel: false,
|
||||
// 背景色
|
||||
backgroundColor: "",
|
||||
// imgSelector
|
||||
imgSelector: "img_thumb",
|
||||
// 是否跨域
|
||||
crossOrigin: true,
|
||||
// 加载配置
|
||||
loadProps: {
|
||||
loading: loadingIcon,
|
||||
error: errorIcon,
|
||||
ratioCalculator: (width, height) => {
|
||||
const minRatio = 3 / 4;
|
||||
const maxRatio = 4 / 3;
|
||||
const curRatio = height / width;
|
||||
if (curRatio < minRatio) {
|
||||
return minRatio;
|
||||
} else if (curRatio > maxRatio) {
|
||||
return maxRatio;
|
||||
} else {
|
||||
return curRatio;
|
||||
}
|
||||
},
|
||||
},
|
||||
// 是否懒加载
|
||||
lazyload: true,
|
||||
align: "center",
|
||||
}
|
||||
|
||||
export const useSharedStore = defineStore("shared", {
|
||||
state: () => ({
|
||||
@@ -10,6 +87,8 @@ export const useSharedStore = defineStore("shared", {
|
||||
theme: Storage.get("theme", "light"),
|
||||
isLogin: false,
|
||||
chatListExtend: Storage.get("chat_list_extend", true),
|
||||
ttsModel: Storage.get("tts_model", ""),
|
||||
waterfallOptions,
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
@@ -74,5 +153,10 @@ export const useSharedStore = defineStore("shared", {
|
||||
setIsLogin(value) {
|
||||
this.isLogin = value;
|
||||
},
|
||||
|
||||
setTtsModel(value) {
|
||||
this.ttsModel = value;
|
||||
Storage.set("tts_model", value);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -464,7 +464,7 @@ onUnmounted(() => {
|
||||
// 初始化数据
|
||||
const initData = () => {
|
||||
// 加载模型
|
||||
httpGet("/api/model/list")
|
||||
httpGet("/api/model/list?type=chat")
|
||||
.then((res) => {
|
||||
models.value = res.data;
|
||||
if (!modelID.value) {
|
||||
@@ -751,12 +751,13 @@ const sendMessage = function () {
|
||||
}
|
||||
// 如果携带了文件,则串上文件地址
|
||||
let content = prompt.value;
|
||||
if (files.value.length === 1) {
|
||||
if (files.value.length > 0) {
|
||||
content += files.value.map((file) => file.url).join(" ");
|
||||
} else if (files.value.length > 1) {
|
||||
showMessageError("当前只支持上传一个文件!");
|
||||
return false;
|
||||
}
|
||||
// else if (files.value.length > 1) {
|
||||
// showMessageError("当前只支持上传一个文件!");
|
||||
// return false;
|
||||
// }
|
||||
// 追加消息
|
||||
chatData.value.push({
|
||||
type: "prompt",
|
||||
|
||||
@@ -11,7 +11,12 @@
|
||||
<el-form-item label="生图模型">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="selectedModel" style="width: 150px" placeholder="请选择模型" @change="changeModel">
|
||||
<el-select
|
||||
v-model="selectedModel"
|
||||
style="width: 150px"
|
||||
placeholder="请选择模型"
|
||||
@change="changeModel"
|
||||
>
|
||||
<el-option v-for="v in models" :label="v.name" :value="v" :key="v.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
@@ -24,7 +29,12 @@
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.quality" style="width: 150px">
|
||||
<el-option v-for="v in qualities" :label="v.name" :value="v.value" :key="v.value" />
|
||||
<el-option
|
||||
v-for="v in qualities"
|
||||
:label="v.name"
|
||||
:value="v.value"
|
||||
:key="v.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
@@ -48,9 +58,18 @@
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.style" style="width: 150px">
|
||||
<el-option v-for="v in styles" :label="v.name" :value="v.value" :key="v.value" />
|
||||
<el-option
|
||||
v-for="v in styles"
|
||||
:label="v.name"
|
||||
:value="v.value"
|
||||
:key="v.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-tooltip content="生动使模型倾向于生成超真实和戏剧性的图像" raw-content placement="right">
|
||||
<el-tooltip
|
||||
content="生动使模型倾向于生成超真实和戏剧性的图像"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -73,7 +92,13 @@
|
||||
</div>
|
||||
|
||||
<el-row class="text-info">
|
||||
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating">
|
||||
<el-button
|
||||
class="generate-btn"
|
||||
size="small"
|
||||
@click="generatePrompt"
|
||||
color="#5865f2"
|
||||
:disabled="isGenerating"
|
||||
>
|
||||
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
|
||||
<span>生成专业绘画指令</span>
|
||||
</el-button>
|
||||
@@ -97,111 +122,148 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-list-box pl-6 pr-6 pb-4 pt-4 h-dvh">
|
||||
<div class="task-list-inner" :style="{ height: listBoxHeight + 'px' }">
|
||||
<div class="task-list-inner">
|
||||
<div class="job-list-box">
|
||||
<h2 class="text-xl">任务列表</h2>
|
||||
<task-list :list="runningJobs" />
|
||||
<template v-if="finishedJobs.length > 0">
|
||||
<h2 class="text-xl">创作记录</h2>
|
||||
<div class="finish-job-list">
|
||||
<div class="finish-job-list mt-3">
|
||||
<div v-if="finishedJobs.length > 0">
|
||||
<v3-waterfall
|
||||
id="waterfall"
|
||||
<Waterfall
|
||||
:list="finishedJobs"
|
||||
srcKey="img_thumb"
|
||||
:gap="20"
|
||||
:bottomGap="-10"
|
||||
:colWidth="colWidth"
|
||||
:distanceToScroll="100"
|
||||
:isLoading="loading"
|
||||
:isOver="isOver"
|
||||
@scrollReachBottom="fetchFinishJobs()"
|
||||
:row-key="waterfallOptions.rowKey"
|
||||
:gutter="waterfallOptions.gutter"
|
||||
:has-around-gutter="waterfallOptions.hasAroundGutter"
|
||||
:width="waterfallOptions.width"
|
||||
:breakpoints="waterfallOptions.breakpoints"
|
||||
:img-selector="waterfallOptions.imgSelector"
|
||||
:background-color="waterfallOptions.backgroundColor"
|
||||
:animation-effect="waterfallOptions.animationEffect"
|
||||
:animation-duration="waterfallOptions.animationDuration"
|
||||
:animation-delay="waterfallOptions.animationDelay"
|
||||
:animation-cancel="waterfallOptions.animationCancel"
|
||||
:lazyload="waterfallOptions.lazyload"
|
||||
:load-props="waterfallOptions.loadProps"
|
||||
:cross-origin="waterfallOptions.crossOrigin"
|
||||
:align="waterfallOptions.align"
|
||||
:is-loading="loading"
|
||||
:is-over="isOver"
|
||||
@afterRender="loading = false"
|
||||
>
|
||||
<template #default="slotProp">
|
||||
<div class="job-item">
|
||||
<el-image
|
||||
v-if="slotProp.item.img_url !== ''"
|
||||
@click="previewImg(slotProp.item)"
|
||||
:src="slotProp.item['img_thumb']"
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="image-slot">正在加载图片</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<el-image v-else-if="slotProp.item.progress === 101">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<div class="err-msg-container">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
|
||||
<template #reference>
|
||||
<el-button type="info">详情</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
|
||||
<template #default="{ item, url }">
|
||||
<div
|
||||
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
|
||||
>
|
||||
<div class="overflow-hidden rounded-lg">
|
||||
<LazyImg
|
||||
:url="url"
|
||||
v-if="item.progress === 100"
|
||||
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
|
||||
@click="previewImg(item)"
|
||||
/>
|
||||
<el-image v-else-if="item.progress === 101">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<div class="err-msg-container">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<el-popover
|
||||
title="错误详情"
|
||||
trigger="click"
|
||||
:width="250"
|
||||
:content="item['err_msg']"
|
||||
placement="top"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button type="info">详情</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-button type="danger" @click="removeImage(item)"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div
|
||||
class="px-4 pt-2 pb-4 border-t border-t-gray-800"
|
||||
v-if="item.progress === 100"
|
||||
>
|
||||
<div
|
||||
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
|
||||
>
|
||||
<div class="flex">
|
||||
<el-tooltip content="取消分享" placement="top" v-if="item.publish">
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="publishImage(item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="分享" placement="top" v-else>
|
||||
<el-button
|
||||
type="success"
|
||||
@click="publishImage(item, true)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="复制提示词" placement="top">
|
||||
<el-button
|
||||
type="info"
|
||||
circle
|
||||
class="copy-prompt"
|
||||
:data-clipboard-text="item.prompt"
|
||||
>
|
||||
<i class="iconfont icon-file"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="removeImage(item)"
|
||||
circle
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<el-image v-else>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<i class="iconfont icon-loading"></i>
|
||||
<span>正在下载图片</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="取消分享" placement="top" v-if="slotProp.item.publish">
|
||||
<el-button type="warning" @click="publishImage(slotProp.item, false)" circle>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="分享" placement="top" v-else>
|
||||
<el-button type="success" @click="publishImage(slotProp.item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="复制提示词" placement="top">
|
||||
<el-button type="info" circle class="copy-prompt" :data-clipboard-text="slotProp.item.prompt">
|
||||
<i class="iconfont icon-file"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Waterfall>
|
||||
|
||||
<template #footer>
|
||||
<div class="no-more-data">
|
||||
<span>没有更多数据了</span>
|
||||
<div class="flex justify-center py-10">
|
||||
<img
|
||||
:src="waterfallOptions.loadProps.loading"
|
||||
class="max-w-[50px] max-h-[50px]"
|
||||
v-if="loading"
|
||||
/>
|
||||
<div v-else>
|
||||
<button
|
||||
class="px-5 py-2 rounded-full bg-purple-700 text-md text-white cursor-pointer hover:bg-purple-800 transition-all duration-300"
|
||||
@click="fetchFinishJobs"
|
||||
v-if="!isOver"
|
||||
>
|
||||
加载更多
|
||||
</button>
|
||||
<div class="no-more-data" v-else>
|
||||
<span class="text-gray-500 mr-2">没有更多数据了</span>
|
||||
<i class="iconfont icon-face"></i>
|
||||
</div>
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" :image="nodata" description="暂无记录" v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- end finish job list-->
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,7 +276,7 @@
|
||||
<el-image-viewer
|
||||
@close="
|
||||
() => {
|
||||
previewURL = '';
|
||||
previewURL = ''
|
||||
}
|
||||
"
|
||||
v-if="previewURL !== ''"
|
||||
@@ -224,280 +286,306 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { Delete, InfoFilled, Picture } from "@element-plus/icons-vue";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import Clipboard from "clipboard";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
import BackTop from "@/components/BackTop.vue";
|
||||
import { showMessageError, showMessageOK } from "@/utils/dialog";
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Delete, InfoFilled, Picture } from '@element-plus/icons-vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import Clipboard from 'clipboard'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import TaskList from '@/components/TaskList.vue'
|
||||
import BackTop from '@/components/BackTop.vue'
|
||||
import { showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
|
||||
import 'vue-waterfall-plugin-next/dist/style.css'
|
||||
|
||||
const listBoxHeight = ref(0);
|
||||
const listBoxHeight = ref(0)
|
||||
// const paramBoxHeight = ref(0)
|
||||
const isLogin = ref(false);
|
||||
const loading = ref(true);
|
||||
const colWidth = ref(220);
|
||||
const isOver = ref(false);
|
||||
const previewURL = ref("");
|
||||
const store = useSharedStore();
|
||||
const models = ref([]);
|
||||
|
||||
const isLogin = ref(false)
|
||||
const loading = ref(true)
|
||||
const isOver = ref(false)
|
||||
const previewURL = ref('')
|
||||
const store = useSharedStore()
|
||||
const models = ref([])
|
||||
const waterfallOptions = store.waterfallOptions
|
||||
const resizeElement = function () {
|
||||
listBoxHeight.value = window.innerHeight - 58;
|
||||
// paramBoxHeight.value = window.innerHeight - 110
|
||||
};
|
||||
resizeElement();
|
||||
window.onresize = () => {
|
||||
resizeElement();
|
||||
};
|
||||
const qualities = [
|
||||
{ name: "标准", value: "standard" },
|
||||
{ name: "高清", value: "hd" },
|
||||
];
|
||||
const dalleSizes = ["1024x1024", "1792x1024", "1024x1792"];
|
||||
const fluxSizes = ["1024x1024", "1024x768", "768x1024", "1280x960", "960x1280", "1366x768", "768x1366"];
|
||||
const sizes = ref(dalleSizes);
|
||||
const styles = [
|
||||
{ name: "生动", value: "vivid" },
|
||||
{ name: "自然", value: "natural" },
|
||||
];
|
||||
const params = ref({
|
||||
quality: "standard",
|
||||
size: "1024x1024",
|
||||
style: "vivid",
|
||||
prompt: "",
|
||||
});
|
||||
listBoxHeight.value = window.innerHeight - 58
|
||||
}
|
||||
|
||||
const finishedJobs = ref([]);
|
||||
const runningJobs = ref([]);
|
||||
const allowPulling = ref(true); // 是否允许轮询
|
||||
const tastPullHandler = ref(null);
|
||||
const power = ref(0);
|
||||
const dallPower = ref(0); // 画一张 SD 图片消耗算力
|
||||
const clipboard = ref(null);
|
||||
const userId = ref(0);
|
||||
const selectedModel = ref(null);
|
||||
resizeElement()
|
||||
window.onresize = () => {
|
||||
resizeElement()
|
||||
}
|
||||
const qualities = [
|
||||
{ name: '标准', value: 'standard' },
|
||||
{ name: '高清', value: 'hd' },
|
||||
]
|
||||
const dalleSizes = ['1024x1024', '1792x1024', '1024x1792']
|
||||
const fluxSizes = ['1024x1024', '1152x896', '896x1152', '1280x960', '1024x576']
|
||||
const sizes = ref(dalleSizes)
|
||||
const styles = [
|
||||
{ name: '生动', value: 'vivid' },
|
||||
{ name: '自然', value: 'natural' },
|
||||
]
|
||||
const params = ref({
|
||||
quality: 'standard',
|
||||
size: '1024x1024',
|
||||
style: 'vivid',
|
||||
prompt: '',
|
||||
})
|
||||
|
||||
const finishedJobs = ref([])
|
||||
const runningJobs = ref([])
|
||||
const allowPulling = ref(true) // 是否允许轮询
|
||||
const downloadPulling = ref(false) // 下载轮询
|
||||
const tastPullHandler = ref(null)
|
||||
const downloadPullHandler = ref(null)
|
||||
const power = ref(0)
|
||||
const dallPower = ref(0) // 画一张 SD 图片消耗算力
|
||||
const clipboard = ref(null)
|
||||
const userId = ref(0)
|
||||
const selectedModel = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
initData();
|
||||
clipboard.value = new Clipboard(".copy-prompt");
|
||||
clipboard.value.on("success", () => {
|
||||
showMessageOK("复制成功!");
|
||||
});
|
||||
initData()
|
||||
clipboard.value = new Clipboard('.copy-prompt')
|
||||
clipboard.value.on('success', () => {
|
||||
showMessageOK('复制成功!')
|
||||
})
|
||||
|
||||
clipboard.value.on("error", () => {
|
||||
showMessageError("复制失败!");
|
||||
});
|
||||
clipboard.value.on('error', () => {
|
||||
showMessageError('复制失败!')
|
||||
})
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
dallPower.value = res.data["dall_power"];
|
||||
dallPower.value = res.data['dall_power']
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("获取系统配置失败:" + e.message);
|
||||
});
|
||||
showMessageError('获取系统配置失败:' + e.message)
|
||||
})
|
||||
|
||||
// 获取模型列表
|
||||
httpGet("/api/dall/models")
|
||||
httpGet('/api/dall/models')
|
||||
.then((res) => {
|
||||
models.value = res.data;
|
||||
selectedModel.value = models.value[0];
|
||||
params.value.model_id = selectedModel.value.id;
|
||||
changeModel(selectedModel.value);
|
||||
models.value = res.data
|
||||
selectedModel.value = models.value[0]
|
||||
params.value.model_id = selectedModel.value.id
|
||||
changeModel(selectedModel.value)
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("获取模型列表失败:" + e.message);
|
||||
});
|
||||
});
|
||||
showMessageError('获取模型列表失败:' + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
clipboard.value.destroy()
|
||||
if (tastPullHandler.value) {
|
||||
clearInterval(tastPullHandler.value);
|
||||
clearInterval(tastPullHandler.value)
|
||||
}
|
||||
});
|
||||
if (downloadPullHandler.value) {
|
||||
clearInterval(downloadPullHandler.value)
|
||||
}
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
power.value = user["power"];
|
||||
userId.value = user.id;
|
||||
isLogin.value = true;
|
||||
page.value = 0;
|
||||
fetchRunningJobs();
|
||||
fetchFinishJobs();
|
||||
power.value = user['power']
|
||||
userId.value = user.id
|
||||
isLogin.value = true
|
||||
page.value = 0
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs()
|
||||
|
||||
// 轮询运行中任务
|
||||
tastPullHandler.value = setInterval(() => {
|
||||
if (allowPulling.value) {
|
||||
fetchRunningJobs();
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
|
||||
// 图片下载轮询
|
||||
downloadPullHandler.value = setInterval(() => {
|
||||
if (downloadPulling.value) {
|
||||
page.value = 0
|
||||
fetchFinishJobs()
|
||||
}
|
||||
}, 5000)
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const fetchRunningJobs = () => {
|
||||
if (!isLogin.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/dall/jobs?finish=false`)
|
||||
.then((res) => {
|
||||
// 如果任务有更新,则更新已完成任务列表
|
||||
if (res.data.items && res.data.items.length !== runningJobs.value.length) {
|
||||
page.value = 0;
|
||||
fetchFinishJobs();
|
||||
page.value = 0
|
||||
fetchFinishJobs()
|
||||
}
|
||||
if (res.data.items.length > 0) {
|
||||
runningJobs.value = res.data.items;
|
||||
runningJobs.value = res.data.items
|
||||
} else {
|
||||
allowPulling.value = false;
|
||||
runningJobs.value = [];
|
||||
allowPulling.value = false
|
||||
runningJobs.value = []
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取任务失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取任务失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(15);
|
||||
const page = ref(1)
|
||||
const pageSize = ref(15)
|
||||
// 获取已完成的任务
|
||||
const fetchFinishJobs = () => {
|
||||
if (!isLogin.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
page.value = page.value + 1;
|
||||
loading.value = true
|
||||
page.value = page.value + 1
|
||||
|
||||
httpGet(`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`)
|
||||
.then((res) => {
|
||||
if (res.data.items.length < pageSize.value) {
|
||||
isOver.value = true;
|
||||
isOver.value = true
|
||||
loading.value = false
|
||||
}
|
||||
const imageList = res.data.items;
|
||||
const imageList = res.data.items
|
||||
let needPulling = false
|
||||
for (let i = 0; i < imageList.length; i++) {
|
||||
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
|
||||
if (imageList[i]['img_url']) {
|
||||
imageList[i]['img_thumb'] = imageList[i]['img_url'] + '?imageView2/4/w/300/h/0/q/75'
|
||||
} else if (imageList[i].progress === 100) {
|
||||
needPulling = true
|
||||
imageList[i]['img_thumb'] = waterfallOptions.loadProps.loading
|
||||
}
|
||||
}
|
||||
// 如果当前是第一页,则开启图片下载轮询
|
||||
if (page.value === 1) {
|
||||
finishedJobs.value = imageList;
|
||||
} else {
|
||||
finishedJobs.value = finishedJobs.value.concat(imageList);
|
||||
downloadPulling.value = needPulling
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
if (page.value === 1) {
|
||||
finishedJobs.value = imageList
|
||||
} else {
|
||||
finishedJobs.value = finishedJobs.value.concat(imageList)
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取任务失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取任务失败:' + e.message)
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 创建绘图任务
|
||||
const promptRef = ref(null);
|
||||
const promptRef = ref(null)
|
||||
const generate = () => {
|
||||
if (params.value.prompt === "") {
|
||||
promptRef.value.focus();
|
||||
return ElMessage.error("请输入绘画提示词!");
|
||||
if (params.value.prompt === '') {
|
||||
promptRef.value.focus()
|
||||
return ElMessage.error('请输入绘画提示词!')
|
||||
}
|
||||
|
||||
if (!isLogin.value) {
|
||||
store.setShowLoginDialog(true);
|
||||
return;
|
||||
store.setShowLoginDialog(true)
|
||||
return
|
||||
}
|
||||
httpPost("/api/dall/image", params.value)
|
||||
httpPost('/api/dall/image', params.value)
|
||||
.then(() => {
|
||||
ElMessage.success("任务执行成功!");
|
||||
power.value -= dallPower.value;
|
||||
ElMessage.success('任务执行成功!')
|
||||
power.value -= dallPower.value
|
||||
// 追加任务列表
|
||||
runningJobs.value.push({
|
||||
prompt: params.value.prompt,
|
||||
progress: 0,
|
||||
});
|
||||
allowPulling.value = true;
|
||||
})
|
||||
allowPulling.value = true
|
||||
isOver.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("任务执行失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('任务执行失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeImage = (item) => {
|
||||
ElMessageBox.confirm("此操作将会删除任务和图片,继续操作码?", "删除提示", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
ElMessageBox.confirm('此操作将会删除任务和图片,继续操作码?', '删除提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet("/api/dall/remove", { id: item.id })
|
||||
httpGet('/api/dall/remove', { id: item.id })
|
||||
.then(() => {
|
||||
ElMessage.success("任务删除成功");
|
||||
page.value = 0;
|
||||
isOver.value = false;
|
||||
fetchFinishJobs();
|
||||
ElMessage.success('任务删除成功')
|
||||
page.value = 0
|
||||
isOver.value = false
|
||||
fetchFinishJobs()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("任务删除失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const previewImg = (item) => {
|
||||
previewURL.value = item.img_url;
|
||||
};
|
||||
previewURL.value = item.img_url
|
||||
}
|
||||
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (item, action) => {
|
||||
let text = "图片发布";
|
||||
let text = '图片发布'
|
||||
if (action === false) {
|
||||
text = "取消发布";
|
||||
text = '取消发布'
|
||||
}
|
||||
httpGet("/api/dall/publish", { id: item.id, action: action })
|
||||
httpGet('/api/dall/publish', { id: item.id, action: action })
|
||||
.then(() => {
|
||||
ElMessage.success(text + "成功");
|
||||
item.publish = action;
|
||||
page.value = 0;
|
||||
isOver.value = false;
|
||||
ElMessage.success(text + '成功')
|
||||
item.publish = action
|
||||
page.value = 0
|
||||
isOver.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error(text + "失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error(text + '失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const isGenerating = ref(false);
|
||||
const isGenerating = ref(false)
|
||||
const generatePrompt = () => {
|
||||
if (params.value.prompt === "") {
|
||||
return showMessageError("请输入原始提示词");
|
||||
if (params.value.prompt === '') {
|
||||
return showMessageError('请输入原始提示词')
|
||||
}
|
||||
isGenerating.value = true;
|
||||
httpPost("/api/prompt/image", { prompt: params.value.prompt })
|
||||
isGenerating.value = true
|
||||
httpPost('/api/prompt/image', { prompt: params.value.prompt })
|
||||
.then((res) => {
|
||||
params.value.prompt = res.data;
|
||||
isGenerating.value = false;
|
||||
params.value.prompt = res.data
|
||||
isGenerating.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("生成提示词失败:" + e.message);
|
||||
isGenerating.value = false;
|
||||
});
|
||||
};
|
||||
showMessageError('生成提示词失败:' + e.message)
|
||||
isGenerating.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const changeModel = (model) => {
|
||||
if (model.value.startsWith("dall")) {
|
||||
sizes.value = dalleSizes;
|
||||
if (model.value.startsWith('dall')) {
|
||||
sizes.value = dalleSizes
|
||||
} else {
|
||||
sizes.value = fluxSizes;
|
||||
sizes.value = fluxSizes
|
||||
}
|
||||
params.value.model_id = selectedModel.value.id;
|
||||
};
|
||||
params.value.model_id = selectedModel.value.id
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/image-dall.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
@import '@/assets/css/image-dall.styl';
|
||||
@import '@/assets/css/custom-scroll.styl';
|
||||
</style>
|
||||
|
||||
@@ -58,7 +58,9 @@
|
||||
<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>
|
||||
<span :class="item.url === curPath ? 'title active' : 'title'">{{
|
||||
item.name
|
||||
}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -77,7 +79,7 @@
|
||||
<el-icon>
|
||||
<UserFilled />
|
||||
</el-icon>
|
||||
<span class="username title">{{ loginUser.nickname }}</span>
|
||||
<span class="username title">账户信息</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!license.de_copy">
|
||||
@@ -108,7 +110,12 @@
|
||||
</div>
|
||||
<el-scrollbar class="right-main">
|
||||
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
|
||||
<el-button @click="router.push('/login')" class="btn-go animate__animated animate__pulse animate__infinite" round>登录</el-button>
|
||||
<el-button
|
||||
@click="router.push('/login')"
|
||||
class="btn-go animate__animated animate__pulse animate__infinite"
|
||||
round
|
||||
>登录</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="content custom-scroll">
|
||||
<router-view :key="routerViewKey" v-slot="{ Component }">
|
||||
@@ -123,7 +130,9 @@
|
||||
|
||||
<el-dialog v-model="showLoginDialog" width="500px" @close="store.setShowLoginDialog(false)">
|
||||
<template #header>
|
||||
<div class="text-center text-xl" style="color: var(--theme-text-color-primary)">登录后解锁功能</div>
|
||||
<div class="text-center text-xl" style="color: var(--theme-text-color-primary)">
|
||||
登录后解锁功能
|
||||
</div>
|
||||
</template>
|
||||
<div class="p-4 pt-2 pb-2">
|
||||
<LoginDialog @success="loginSuccess" @hide="store.setShowLoginDialog(false)" />
|
||||
@@ -133,33 +142,33 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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 ConfigDialog from "@/components/UserInfoDialog.vue";
|
||||
import { showMessageError } from "@/utils/dialog";
|
||||
import LoginDialog from "@/components/LoginDialog.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 ConfigDialog from '@/components/UserInfoDialog.vue'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import LoginDialog from '@/components/LoginDialog.vue'
|
||||
|
||||
const router = useRouter();
|
||||
const logo = ref("");
|
||||
const mainNavs = ref([]);
|
||||
const moreNavs = ref([]);
|
||||
const curPath = ref();
|
||||
const router = useRouter()
|
||||
const logo = ref('')
|
||||
const mainNavs = ref([])
|
||||
const moreNavs = ref([])
|
||||
const curPath = ref()
|
||||
|
||||
const title = ref("");
|
||||
const store = useSharedStore();
|
||||
const loginUser = ref({});
|
||||
const routerViewKey = ref(0);
|
||||
const showConfigDialog = ref(false);
|
||||
const license = ref({ de_copy: true });
|
||||
const showLoginDialog = ref(false);
|
||||
const githubURL = ref(process.env.VUE_APP_GITHUB_URL);
|
||||
const title = ref('')
|
||||
const store = useSharedStore()
|
||||
const loginUser = ref({})
|
||||
const routerViewKey = ref(0)
|
||||
const showConfigDialog = ref(false)
|
||||
const license = ref({ de_copy: true })
|
||||
const showLoginDialog = ref(false)
|
||||
const githubURL = ref(process.env.VUE_APP_GITHUB_URL)
|
||||
|
||||
/**
|
||||
* 从路径名中提取第一个路径段
|
||||
@@ -167,123 +176,123 @@ const githubURL = ref(process.env.VUE_APP_GITHUB_URL);
|
||||
* @returns 第一个路径段(不含斜杠),例如 'chat',如果不存在则返回 null
|
||||
*/
|
||||
const extractFirstSegment = (pathname) => {
|
||||
const segments = pathname.split("/").filter((segment) => segment.length > 0);
|
||||
return segments.length > 0 ? segments[0] : null;
|
||||
};
|
||||
const segments = pathname.split('/').filter((segment) => segment.length > 0)
|
||||
return segments.length > 0 ? segments[0] : null
|
||||
}
|
||||
const getFirstPathSegment = (url) => {
|
||||
try {
|
||||
// 尝试使用 URL 构造函数解析完整的 URL
|
||||
const parsedUrl = new URL(url);
|
||||
return extractFirstSegment(parsedUrl.pathname);
|
||||
const parsedUrl = new URL(url)
|
||||
return extractFirstSegment(parsedUrl.pathname)
|
||||
} catch (error) {
|
||||
// 如果解析失败,假设是相对路径,使用当前窗口的位置作为基准
|
||||
if (typeof window !== "undefined") {
|
||||
const parsedUrl = new URL(url, window.location.origin);
|
||||
return extractFirstSegment(parsedUrl.pathname);
|
||||
if (typeof window !== 'undefined') {
|
||||
const parsedUrl = new URL(url, window.location.origin)
|
||||
return extractFirstSegment(parsedUrl.pathname)
|
||||
}
|
||||
// 如果无法解析,返回 null
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const stars = computed(() => {
|
||||
return 1000;
|
||||
});
|
||||
return 1000
|
||||
})
|
||||
|
||||
watch(
|
||||
() => store.showLoginDialog,
|
||||
(newValue) => {
|
||||
showLoginDialog.value = newValue;
|
||||
showLoginDialog.value = newValue
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
// 监听路由变化;
|
||||
router.beforeEach((to, from, next) => {
|
||||
curPath.value = to.path;
|
||||
next();
|
||||
});
|
||||
curPath.value = to.path
|
||||
next()
|
||||
})
|
||||
|
||||
if (curPath.value === "/external") {
|
||||
curPath.value = router.currentRoute.value.query.url;
|
||||
if (curPath.value === '/external') {
|
||||
curPath.value = router.currentRoute.value.query.url
|
||||
}
|
||||
const changeNav = (item) => {
|
||||
curPath.value = item.url;
|
||||
if (item.url.indexOf("http") !== -1) {
|
||||
curPath.value = item.url
|
||||
if (item.url.indexOf('http') !== -1) {
|
||||
// 外部链接
|
||||
router.push({ path: "/external", query: { url: item.url, title: item.name } });
|
||||
router.push({ path: '/external', query: { url: item.url, title: item.name } })
|
||||
} else {
|
||||
// 路由切换,确保路径变化
|
||||
if (router.currentRoute.value.path !== item.url) {
|
||||
router.push(item.url).then(() => {
|
||||
// 刷新 `routerViewKey` 触发视图重新渲染
|
||||
routerViewKey.value += 1;
|
||||
});
|
||||
routerViewKey.value += 1
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
curPath.value = router.currentRoute.value.path;
|
||||
curPath.value = router.currentRoute.value.path
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
logo.value = res.data.logo;
|
||||
title.value = res.data.title;
|
||||
logo.value = res.data.logo
|
||||
title.value = res.data.title
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
// 获取菜单
|
||||
httpGet("/api/menu/list")
|
||||
httpGet('/api/menu/list')
|
||||
.then((res) => {
|
||||
mainNavs.value = res.data;
|
||||
mainNavs.value = res.data
|
||||
// 根据窗口的高度计算应该显示多少菜单
|
||||
const rows = Math.floor((window.innerHeight - 100) / 90);
|
||||
const rows = Math.floor((window.innerHeight - 100) / 90)
|
||||
if (res.data.length > rows) {
|
||||
mainNavs.value = res.data.slice(0, rows);
|
||||
moreNavs.value = res.data.slice(rows);
|
||||
mainNavs.value = res.data.slice(0, rows)
|
||||
moreNavs.value = res.data.slice(rows)
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统菜单失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('获取系统菜单失败:' + e.message)
|
||||
})
|
||||
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
license.value = res.data;
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
license.value = { de_copy: false };
|
||||
showMessageError("获取 License 配置:" + e.message);
|
||||
});
|
||||
curPath.value = "/" + getFirstPathSegment(window.location.href);
|
||||
init();
|
||||
});
|
||||
license.value = { de_copy: false }
|
||||
showMessageError('获取 License 配置:' + e.message)
|
||||
})
|
||||
curPath.value = '/' + getFirstPathSegment(window.location.href)
|
||||
init()
|
||||
})
|
||||
|
||||
const init = () => {
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
loginUser.value = user;
|
||||
loginUser.value = user
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const logout = function () {
|
||||
httpGet("/api/user/logout")
|
||||
httpGet('/api/user/logout')
|
||||
.then(() => {
|
||||
removeUserToken();
|
||||
router.push("/login");
|
||||
removeUserToken()
|
||||
router.push('/login')
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("注销失败!");
|
||||
});
|
||||
};
|
||||
ElMessage.error('注销失败!')
|
||||
})
|
||||
}
|
||||
|
||||
const loginSuccess = () => {
|
||||
init();
|
||||
store.setShowLoginDialog(false);
|
||||
init()
|
||||
store.setShowLoginDialog(false)
|
||||
// 刷新组件
|
||||
routerViewKey.value += 1;
|
||||
};
|
||||
routerViewKey.value += 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,9 +12,18 @@
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.sampler" style="width: 150px">
|
||||
<el-option v-for="item in samplers" :label="item" :value="item" :key="item" />
|
||||
<el-option
|
||||
v-for="item in samplers"
|
||||
:label="item"
|
||||
:value="item"
|
||||
:key="item"
|
||||
/>
|
||||
</el-select>
|
||||
<el-tooltip content="出图效果比较好的一般是 Euler 和 DPM 系列算法" raw-content placement="right">
|
||||
<el-tooltip
|
||||
content="出图效果比较好的一般是 Euler 和 DPM 系列算法"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -29,7 +38,12 @@
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.scheduler" style="width: 150px">
|
||||
<el-option v-for="item in schedulers" :label="item" :value="item" :key="item" />
|
||||
<el-option
|
||||
v-for="item in schedulers"
|
||||
:label="item"
|
||||
:value="item"
|
||||
:key="item"
|
||||
/>
|
||||
</el-select>
|
||||
<el-tooltip content="推荐自动或者 Karras" raw-content placement="right">
|
||||
<el-icon class="info-icon">
|
||||
@@ -63,7 +77,11 @@
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.steps" />
|
||||
<el-tooltip content="值越大则代表细节越多,同时也意味着出图速度越慢" raw-content placement="right">
|
||||
<el-tooltip
|
||||
content="值越大则代表细节越多,同时也意味着出图速度越慢"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -78,7 +96,11 @@
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.cfg_scale" />
|
||||
<el-tooltip content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果" raw-content placement="right">
|
||||
<el-tooltip
|
||||
content="提示词引导系数,图像在多大程度上服从提示词<br/> 较低值会产生更有创意的结果"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -93,7 +115,11 @@
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.seed" />
|
||||
<el-tooltip content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子" raw-content placement="right">
|
||||
<el-tooltip
|
||||
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -113,8 +139,16 @@
|
||||
<el-form-item label="高清修复">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-switch v-model="params.hd_fix" style="--el-switch-on-color: #47fff1" size="large" />
|
||||
<el-tooltip content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节" raw-content placement="right">
|
||||
<el-switch
|
||||
v-model="params.hd_fix"
|
||||
style="--el-switch-on-color: #47fff1"
|
||||
size="large"
|
||||
/>
|
||||
<el-tooltip
|
||||
content="先以较小的分辨率生成图像,接着方法图像<br />然后在不更改构图的情况下再修改细节"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon style="margin-left: 10px; top: 12px">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -129,8 +163,17 @@
|
||||
<el-form-item label="重绘幅度">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1" style="width: 180px; --el-slider-main-bg-color: #47fff1" />
|
||||
<el-tooltip content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像" raw-content placement="right">
|
||||
<el-slider
|
||||
v-model.number="params.hd_redraw_rate"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
style="width: 180px; --el-slider-main-bg-color: #47fff1"
|
||||
/>
|
||||
<el-tooltip
|
||||
content="决定算法对图像内容的影响程度<br />较大的值将得到越有创意的图像"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -145,9 +188,18 @@
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.hd_scale_alg" style="width: 176px">
|
||||
<el-option v-for="item in scaleAlg" :label="item" :value="item" :key="item" />
|
||||
<el-option
|
||||
v-for="item in scaleAlg"
|
||||
:label="item"
|
||||
:value="item"
|
||||
:key="item"
|
||||
/>
|
||||
</el-select>
|
||||
<el-tooltip content="高清修复放大算法,主流算法有Latent和ESRGAN_4x" raw-content placement="right">
|
||||
<el-tooltip
|
||||
content="高清修复放大算法,主流算法有Latent和ESRGAN_4x"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -162,7 +214,11 @@
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.hd_scale" />
|
||||
<el-tooltip content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子" raw-content placement="right">
|
||||
<el-tooltip
|
||||
content="随机数种子,相同的种子会得到相同的结果<br/> 设置为 -1 则每次随机生成种子"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -177,7 +233,11 @@
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-input v-model.number="params.hd_steps" />
|
||||
<el-tooltip content="重绘迭代步数,如果设置为0,则设置跟原图相同的迭代步数" raw-content placement="right">
|
||||
<el-tooltip
|
||||
content="重绘迭代步数,如果设置为0,则设置跟原图相同的迭代步数"
|
||||
raw-content
|
||||
placement="right"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
@@ -201,7 +261,13 @@
|
||||
</div>
|
||||
|
||||
<el-row class="text-info">
|
||||
<el-button class="generate-btn" size="small" @click="generatePrompt" color="#5865f2" :disabled="isGenerating">
|
||||
<el-button
|
||||
class="generate-btn"
|
||||
size="small"
|
||||
@click="generatePrompt"
|
||||
color="#5865f2"
|
||||
:disabled="isGenerating"
|
||||
>
|
||||
<i class="iconfont icon-chuangzuo" style="margin-right: 5px"></i>
|
||||
<span>生成专业绘画指令</span>
|
||||
</el-button>
|
||||
@@ -216,7 +282,12 @@
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="param-line">
|
||||
<el-input v-model="params.neg_prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea" placeholder="反向提示词" />
|
||||
<el-input
|
||||
v-model="params.neg_prompt"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
type="textarea"
|
||||
placeholder="反向提示词"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-info">
|
||||
@@ -243,61 +314,138 @@
|
||||
<task-list :list="runningJobs" />
|
||||
<template v-if="finishedJobs.length > 0">
|
||||
<h2 class="text-xl">创作记录</h2>
|
||||
<div class="finish-job-list">
|
||||
<div class="finish-job-list mt-3">
|
||||
<div v-if="finishedJobs.length > 0">
|
||||
<v3-waterfall
|
||||
id="waterfall"
|
||||
<Waterfall
|
||||
:list="finishedJobs"
|
||||
srcKey="img_thumb"
|
||||
:gap="20"
|
||||
:bottomGap="-10"
|
||||
:colWidth="colWidth"
|
||||
:distanceToScroll="100"
|
||||
:isLoading="loading"
|
||||
:isOver="isOver"
|
||||
@scrollReachBottom="fetchFinishJobs()"
|
||||
:row-key="waterfallOptions.rowKey"
|
||||
:gutter="waterfallOptions.gutter"
|
||||
:has-around-gutter="waterfallOptions.hasAroundGutter"
|
||||
:width="waterfallOptions.width"
|
||||
:breakpoints="waterfallOptions.breakpoints"
|
||||
:img-selector="waterfallOptions.imgSelector"
|
||||
:background-color="waterfallOptions.backgroundColor"
|
||||
:animation-effect="waterfallOptions.animationEffect"
|
||||
:animation-duration="waterfallOptions.animationDuration"
|
||||
:animation-delay="waterfallOptions.animationDelay"
|
||||
:animation-cancel="waterfallOptions.animationCancel"
|
||||
:lazyload="waterfallOptions.lazyload"
|
||||
:load-props="waterfallOptions.loadProps"
|
||||
:cross-origin="waterfallOptions.crossOrigin"
|
||||
:align="waterfallOptions.align"
|
||||
:is-loading="loading"
|
||||
:is-over="isOver"
|
||||
@afterRender="loading = false"
|
||||
>
|
||||
<template #default="slotProp">
|
||||
<div class="job-item animate">
|
||||
<el-image v-if="slotProp.item.progress === 101">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<div class="err-msg-container">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
|
||||
<template #reference>
|
||||
<el-button type="info">详情</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
|
||||
<template #default="{ item, url }">
|
||||
<div
|
||||
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
|
||||
>
|
||||
<div class="overflow-hidden rounded-lg">
|
||||
<LazyImg
|
||||
:url="url"
|
||||
v-if="item.progress === 100"
|
||||
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
|
||||
@click="showTask(item)"
|
||||
/>
|
||||
<el-image v-else-if="item.progress === 101">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<div class="err-msg-container">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<el-popover
|
||||
title="错误详情"
|
||||
trigger="click"
|
||||
:width="250"
|
||||
:content="item['err_msg']"
|
||||
placement="top"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button type="info">详情</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
<el-button type="danger" @click="removeImage(item)"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div
|
||||
class="px-4 pt-2 pb-4 border-t border-t-gray-800"
|
||||
v-if="item.progress === 100"
|
||||
>
|
||||
<div
|
||||
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
|
||||
>
|
||||
<div class="flex">
|
||||
<el-tooltip content="取消分享" placement="top" v-if="item.publish">
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="publishImage(item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="分享" placement="top" v-else>
|
||||
<el-button
|
||||
type="success"
|
||||
@click="publishImage(item, true)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="复制提示词" placement="top">
|
||||
<el-button
|
||||
type="info"
|
||||
circle
|
||||
class="copy-prompt"
|
||||
:data-clipboard-text="item.prompt"
|
||||
>
|
||||
<i class="iconfont icon-file"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="removeImage(item)"
|
||||
circle
|
||||
/>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else>
|
||||
<el-image :src="slotProp.item['img_thumb']" @click="showTask(slotProp.item)" fit="cover" loading="lazy" />
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle />
|
||||
<el-button type="warning" v-if="slotProp.item.publish" @click="publishImage(slotProp.item, false)" circle>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage(slotProp.item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Waterfall>
|
||||
|
||||
<template #footer>
|
||||
<div class="no-more-data">
|
||||
<span>没有更多数据了</span>
|
||||
<div class="flex justify-center py-10">
|
||||
<img
|
||||
:src="waterfallOptions.loadProps.loading"
|
||||
class="max-w-[50px] max-h-[50px]"
|
||||
v-if="loading"
|
||||
/>
|
||||
<div v-else>
|
||||
<button
|
||||
class="px-5 py-2 rounded-full bg-purple-700 text-md text-white cursor-pointer hover:bg-purple-800 transition-all duration-300"
|
||||
@click="fetchFinishJobs"
|
||||
v-if="!isOver"
|
||||
>
|
||||
加载更多
|
||||
</button>
|
||||
<div class="no-more-data" v-else>
|
||||
<span class="text-gray-500 mr-2">没有更多数据了</span>
|
||||
<i class="iconfont icon-face"></i>
|
||||
</div>
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty :image-size="100" v-else :image="nodata" description="暂无记录" />
|
||||
</div>
|
||||
@@ -312,48 +460,63 @@
|
||||
</div>
|
||||
|
||||
<!-- 任务详情弹框 -->
|
||||
<sd-task-view v-model="showTaskDialog" :data="item" @drawSame="copyParams" @close="showTaskDialog = false" />
|
||||
<sd-task-view
|
||||
v-model="showTaskDialog"
|
||||
:data="item"
|
||||
@drawSame="copyParams"
|
||||
@close="showTaskDialog = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||
import { Delete, DocumentCopy, InfoFilled, Orange } from "@element-plus/icons-vue";
|
||||
import nodata from "@/assets/img/no-data.png";
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Delete, DocumentCopy, InfoFilled, Orange } from '@element-plus/icons-vue'
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import Clipboard from "clipboard";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import { useRouter } from "vue-router";
|
||||
import { getSessionId } from "@/store/session";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import TaskList from "@/components/TaskList.vue";
|
||||
import BackTop from "@/components/BackTop.vue";
|
||||
import { showMessageError } from "@/utils/dialog";
|
||||
import SdTaskView from "@/components/SdTaskView.vue";
|
||||
const listBoxHeight = ref(0);
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import Clipboard from 'clipboard'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import TaskList from '@/components/TaskList.vue'
|
||||
import BackTop from '@/components/BackTop.vue'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import SdTaskView from '@/components/SdTaskView.vue'
|
||||
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
|
||||
import 'vue-waterfall-plugin-next/dist/style.css'
|
||||
|
||||
const listBoxHeight = ref(0)
|
||||
// const paramBoxHeight = ref(0)
|
||||
const fullImgHeight = ref(window.innerHeight - 60);
|
||||
const showTaskDialog = ref(false);
|
||||
const item = ref({});
|
||||
const isLogin = ref(false);
|
||||
const loading = ref(true);
|
||||
const colWidth = ref(220);
|
||||
const store = useSharedStore();
|
||||
const showTaskDialog = ref(false)
|
||||
const item = ref({})
|
||||
const isLogin = ref(false)
|
||||
const loading = ref(true)
|
||||
const store = useSharedStore()
|
||||
const waterfallOptions = store.waterfallOptions
|
||||
|
||||
const resizeElement = function () {
|
||||
listBoxHeight.value = window.innerHeight - 80;
|
||||
listBoxHeight.value = window.innerHeight - 80
|
||||
// paramBoxHeight.value = window.innerHeight - 200
|
||||
};
|
||||
resizeElement();
|
||||
}
|
||||
resizeElement()
|
||||
window.onresize = () => {
|
||||
resizeElement();
|
||||
};
|
||||
const samplers = ["Euler a", "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE", "UniPC", "Restart"];
|
||||
const schedulers = ["Automatic", "Karras", "Exponential", "Uniform"];
|
||||
const scaleAlg = ["Latent", "ESRGAN_4x", "R-ESRGAN 4x+", "SwinIR_4x", "LDSR"];
|
||||
resizeElement()
|
||||
}
|
||||
const samplers = [
|
||||
'Euler a',
|
||||
'DPM++ 2S a',
|
||||
'DPM++ 2M',
|
||||
'DPM++ SDE',
|
||||
'DPM++ 2M SDE',
|
||||
'UniPC',
|
||||
'Restart',
|
||||
]
|
||||
const schedulers = ['Automatic', 'Karras', 'Exponential', 'Uniform']
|
||||
const scaleAlg = ['Latent', 'ESRGAN_4x', 'R-ESRGAN 4x+', 'SwinIR_4x', 'LDSR']
|
||||
const params = ref({
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
@@ -367,227 +530,229 @@ const params = ref({
|
||||
hd_scale: 2,
|
||||
hd_scale_alg: scaleAlg[0],
|
||||
hd_steps: 0,
|
||||
prompt: "",
|
||||
neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
|
||||
});
|
||||
prompt: '',
|
||||
neg_prompt:
|
||||
'nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet',
|
||||
})
|
||||
|
||||
const runningJobs = ref([]);
|
||||
const finishedJobs = ref([]);
|
||||
const allowPulling = ref(true); // 是否允许轮询
|
||||
const tastPullHandler = ref(null);
|
||||
const router = useRouter();
|
||||
const runningJobs = ref([])
|
||||
const finishedJobs = ref([])
|
||||
const allowPulling = ref(true) // 是否允许轮询
|
||||
const tastPullHandler = ref(null)
|
||||
const router = useRouter()
|
||||
// 检查是否有画同款的参数
|
||||
const _params = router.currentRoute.value.params["copyParams"];
|
||||
const _params = router.currentRoute.value.params['copyParams']
|
||||
if (_params) {
|
||||
params.value = JSON.parse(_params);
|
||||
params.value = JSON.parse(_params)
|
||||
}
|
||||
const power = ref(0);
|
||||
const sdPower = ref(0); // 画一张 SD 图片消耗算力
|
||||
const power = ref(0)
|
||||
const sdPower = ref(0) // 画一张 SD 图片消耗算力
|
||||
|
||||
const userId = ref(0);
|
||||
const clipboard = ref(null);
|
||||
const userId = ref(0)
|
||||
const clipboard = ref(null)
|
||||
onMounted(() => {
|
||||
initData();
|
||||
clipboard.value = new Clipboard(".copy-prompt-sd");
|
||||
clipboard.value.on("success", () => {
|
||||
ElMessage.success("复制成功!");
|
||||
});
|
||||
initData()
|
||||
clipboard.value = new Clipboard('.copy-prompt-sd')
|
||||
clipboard.value.on('success', () => {
|
||||
ElMessage.success('复制成功!')
|
||||
})
|
||||
|
||||
clipboard.value.on("error", () => {
|
||||
ElMessage.error("复制失败!");
|
||||
});
|
||||
clipboard.value.on('error', () => {
|
||||
ElMessage.error('复制失败!')
|
||||
})
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
sdPower.value = res.data.sd_power;
|
||||
params.value.neg_prompt = res.data.sd_neg_prompt;
|
||||
sdPower.value = res.data.sd_power
|
||||
params.value.neg_prompt = res.data.sd_neg_prompt
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message);
|
||||
});
|
||||
});
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clipboard.value.destroy();
|
||||
clipboard.value.destroy()
|
||||
if (tastPullHandler.value) {
|
||||
clearInterval(tastPullHandler.value);
|
||||
clearInterval(tastPullHandler.value)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
power.value = user["power"];
|
||||
userId.value = user.id;
|
||||
isLogin.value = true;
|
||||
page.value = 0;
|
||||
fetchRunningJobs();
|
||||
fetchFinishJobs();
|
||||
power.value = user['power']
|
||||
userId.value = user.id
|
||||
isLogin.value = true
|
||||
page.value = 0
|
||||
fetchRunningJobs()
|
||||
fetchFinishJobs()
|
||||
|
||||
tastPullHandler.value = setInterval(() => {
|
||||
if (allowPulling.value) {
|
||||
fetchRunningJobs();
|
||||
fetchRunningJobs()
|
||||
}
|
||||
}, 5000);
|
||||
}, 5000)
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const fetchRunningJobs = () => {
|
||||
if (!isLogin.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// 获取运行中的任务
|
||||
httpGet(`/api/sd/jobs?finish=0`)
|
||||
.then((res) => {
|
||||
if (runningJobs.value.length !== res.data.items.length) {
|
||||
page.value = 0;
|
||||
fetchFinishJobs();
|
||||
page.value = 0
|
||||
fetchFinishJobs()
|
||||
}
|
||||
if (runningJobs.value.length === 0) {
|
||||
allowPulling.value = false;
|
||||
allowPulling.value = false
|
||||
}
|
||||
runningJobs.value = res.data.items;
|
||||
runningJobs.value = res.data.items
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取任务失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取任务失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const page = ref(0);
|
||||
const pageSize = ref(20);
|
||||
const isOver = ref(false);
|
||||
const page = ref(0)
|
||||
const pageSize = ref(20)
|
||||
const isOver = ref(false)
|
||||
// 获取已完成的任务
|
||||
const fetchFinishJobs = () => {
|
||||
if (!isLogin.value || isOver.value === true) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
loading.value = true;
|
||||
page.value = page.value + 1;
|
||||
loading.value = true
|
||||
page.value = page.value + 1
|
||||
|
||||
httpGet(`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`)
|
||||
.then((res) => {
|
||||
if (res.data.items.length < pageSize.value) {
|
||||
isOver.value = true;
|
||||
isOver.value = true
|
||||
loading.value = false
|
||||
}
|
||||
const imageList = res.data.items;
|
||||
const imageList = res.data.items
|
||||
for (let i = 0; i < imageList.length; i++) {
|
||||
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
|
||||
imageList[i]['img_thumb'] = imageList[i]['img_url'] + '?imageView2/4/w/300/h/0/q/75'
|
||||
}
|
||||
if (page.value === 1) {
|
||||
finishedJobs.value = imageList;
|
||||
finishedJobs.value = imageList
|
||||
} else {
|
||||
finishedJobs.value = finishedJobs.value.concat(imageList);
|
||||
finishedJobs.value = finishedJobs.value.concat(imageList)
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取任务失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取任务失败:' + e.message)
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 创建绘图任务
|
||||
const promptRef = ref(null);
|
||||
const promptRef = ref(null)
|
||||
const generate = () => {
|
||||
if (params.value.prompt === "") {
|
||||
promptRef.value.focus();
|
||||
return ElMessage.error("请输入绘画提示词!");
|
||||
if (params.value.prompt === '') {
|
||||
promptRef.value.focus()
|
||||
return ElMessage.error('请输入绘画提示词!')
|
||||
}
|
||||
|
||||
if (!isLogin.value) {
|
||||
store.setShowLoginDialog(true);
|
||||
return;
|
||||
store.setShowLoginDialog(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (!params.value.seed) {
|
||||
params.value.seed = -1;
|
||||
params.value.seed = -1
|
||||
}
|
||||
params.value.session_id = getSessionId();
|
||||
httpPost("/api/sd/image", params.value)
|
||||
params.value.session_id = getSessionId()
|
||||
httpPost('/api/sd/image', params.value)
|
||||
.then(() => {
|
||||
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...");
|
||||
power.value -= sdPower.value;
|
||||
allowPulling.value = true;
|
||||
ElMessage.success('绘画任务推送成功,请耐心等待任务执行...')
|
||||
power.value -= sdPower.value
|
||||
allowPulling.value = true
|
||||
runningJobs.value.push({
|
||||
progress: 0,
|
||||
});
|
||||
})
|
||||
isOver.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("任务推送失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('任务推送失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const showTask = (row) => {
|
||||
item.value = row;
|
||||
showTaskDialog.value = true;
|
||||
};
|
||||
item.value = row
|
||||
showTaskDialog.value = true
|
||||
}
|
||||
|
||||
const copyParams = (row) => {
|
||||
params.value = row.params;
|
||||
showTaskDialog.value = false;
|
||||
};
|
||||
params.value = row.params
|
||||
showTaskDialog.value = false
|
||||
}
|
||||
|
||||
const removeImage = (item) => {
|
||||
ElMessageBox.confirm("此操作将会删除任务和图片,继续操作码?", "删除提示", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
ElMessageBox.confirm('此操作将会删除任务和图片,继续操作码?', '删除提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet("/api/sd/remove", { id: item.id })
|
||||
httpGet('/api/sd/remove', { id: item.id })
|
||||
.then(() => {
|
||||
ElMessage.success("任务删除成功");
|
||||
page.value = 0;
|
||||
isOver.value = false;
|
||||
fetchFinishJobs();
|
||||
ElMessage.success('任务删除成功')
|
||||
page.value = 0
|
||||
isOver.value = false
|
||||
fetchFinishJobs()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("任务删除失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('任务删除失败:' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// 发布图片到作品墙
|
||||
const publishImage = (item, action) => {
|
||||
let text = "图片发布";
|
||||
let text = '图片发布'
|
||||
if (action === false) {
|
||||
text = "取消发布";
|
||||
text = '取消发布'
|
||||
}
|
||||
httpGet("/api/sd/publish", { id: item.id, action: action })
|
||||
httpGet('/api/sd/publish', { id: item.id, action: action })
|
||||
.then(() => {
|
||||
ElMessage.success(text + "成功");
|
||||
item.publish = action;
|
||||
page.value = 0;
|
||||
isOver.value = false;
|
||||
item.publish = action;
|
||||
ElMessage.success(text + '成功')
|
||||
item.publish = action
|
||||
page.value = 0
|
||||
isOver.value = false
|
||||
item.publish = action
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error(text + "失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error(text + '失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const isGenerating = ref(false);
|
||||
const isGenerating = ref(false)
|
||||
const generatePrompt = () => {
|
||||
if (params.value.prompt === "") {
|
||||
return showMessageError("请输入原始提示词");
|
||||
if (params.value.prompt === '') {
|
||||
return showMessageError('请输入原始提示词')
|
||||
}
|
||||
isGenerating.value = true;
|
||||
httpPost("/api/prompt/image", { prompt: params.value.prompt })
|
||||
isGenerating.value = true
|
||||
httpPost('/api/prompt/image', { prompt: params.value.prompt })
|
||||
.then((res) => {
|
||||
params.value.prompt = res.data;
|
||||
isGenerating.value = false;
|
||||
params.value.prompt = res.data
|
||||
isGenerating.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("生成提示词失败:" + e.message);
|
||||
isGenerating.value = false;
|
||||
});
|
||||
};
|
||||
showMessageError('生成提示词失败:' + e.message)
|
||||
isGenerating.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/image-sd.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
@import '@/assets/css/image-sd.styl';
|
||||
@import '@/assets/css/custom-scroll.styl';
|
||||
</style>
|
||||
|
||||
@@ -11,149 +11,238 @@
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="waterfall" :style="{ height: listBoxHeight + 'px' }" id="waterfall-box">
|
||||
<v3-waterfall
|
||||
<div
|
||||
class="waterfall"
|
||||
:style="{ height: listBoxHeight + 'px' }"
|
||||
id="waterfall-box"
|
||||
>
|
||||
<Waterfall
|
||||
v-if="imgType === 'mj'"
|
||||
id="waterfall"
|
||||
id="waterfall-mj"
|
||||
:list="data['mj']"
|
||||
srcKey="img_thumb"
|
||||
:gap="12"
|
||||
:bottomGap="-5"
|
||||
:colWidth="colWidth"
|
||||
:distanceToScroll="100"
|
||||
:isLoading="loading"
|
||||
:isOver="isOver"
|
||||
@scrollReachBottom="getNext"
|
||||
:row-key="waterfallOptions.rowKey"
|
||||
:gutter="waterfallOptions.gutter"
|
||||
:has-around-gutter="waterfallOptions.hasAroundGutter"
|
||||
:width="waterfallOptions.width"
|
||||
:breakpoints="waterfallOptions.breakpoints"
|
||||
:img-selector="waterfallOptions.imgSelector"
|
||||
:background-color="waterfallOptions.backgroundColor"
|
||||
:animation-effect="waterfallOptions.animationEffect"
|
||||
:animation-duration="waterfallOptions.animationDuration"
|
||||
:animation-delay="waterfallOptions.animationDelay"
|
||||
:animation-cancel="waterfallOptions.animationCancel"
|
||||
:lazyload="waterfallOptions.lazyload"
|
||||
:load-props="waterfallOptions.loadProps"
|
||||
:cross-origin="waterfallOptions.crossOrigin"
|
||||
:align="waterfallOptions.align"
|
||||
:is-loading="loading"
|
||||
:is-over="isOver"
|
||||
@afterRender="loading = false"
|
||||
>
|
||||
<template #default="slotProp">
|
||||
<div class="list-item">
|
||||
<div class="image">
|
||||
<el-image
|
||||
:src="slotProp.item['img_thumb']"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[slotProp.item['img_url']]"
|
||||
:preview-teleported="true"
|
||||
:initial-index="10"
|
||||
loading="lazy"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="image-slot">正在加载图片</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<template #default="{ item, url }">
|
||||
<div
|
||||
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
|
||||
>
|
||||
<div class="overflow-hidden rounded-lg">
|
||||
<LazyImg
|
||||
:url="url"
|
||||
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
|
||||
@click="previewImg(item)"
|
||||
/>
|
||||
</div>
|
||||
<div class="opt">
|
||||
<el-tooltip class="box-item" content="复制提示词" placement="top">
|
||||
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
|
||||
<div
|
||||
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
|
||||
>
|
||||
<div class="opt">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="复制提示词"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
type="info"
|
||||
circle
|
||||
class="copy-prompt-wall"
|
||||
:data-clipboard-text="item.prompt"
|
||||
>
|
||||
<i class="iconfont icon-file"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="box-item" content="画同款" placement="top">
|
||||
<i class="iconfont icon-palette-pen" @click="drawSameMj(slotProp.item)"></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="画同款"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@click="drawSameMj(item)"
|
||||
>
|
||||
<i class="iconfont icon-palette"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
</Waterfall>
|
||||
|
||||
<v3-waterfall
|
||||
v-else-if="imgType === 'dall'"
|
||||
id="waterfall"
|
||||
:list="data['dall']"
|
||||
srcKey="img_thumb"
|
||||
:gap="12"
|
||||
:bottomGap="-5"
|
||||
:colWidth="colWidth"
|
||||
:distanceToScroll="100"
|
||||
:isLoading="loading"
|
||||
:isOver="isOver"
|
||||
@scrollReachBottom="getNext"
|
||||
>
|
||||
<template #default="slotProp">
|
||||
<div class="list-item">
|
||||
<div class="image">
|
||||
<el-image
|
||||
:src="slotProp.item['img_thumb']"
|
||||
:zoom-rate="1.2"
|
||||
:preview-src-list="[slotProp.item['img_url']]"
|
||||
:preview-teleported="true"
|
||||
:initial-index="10"
|
||||
loading="lazy"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="image-slot">正在加载图片</div>
|
||||
</template>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="opt">
|
||||
<el-tooltip class="box-item" content="复制提示词" placement="top">
|
||||
<el-icon class="copy-prompt-wall" :data-clipboard-text="slotProp.item.prompt">
|
||||
<DocumentCopy />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
|
||||
<v3-waterfall
|
||||
v-else
|
||||
id="waterfall"
|
||||
<Waterfall
|
||||
v-if="imgType === 'sd'"
|
||||
id="waterfall-sd"
|
||||
:list="data['sd']"
|
||||
srcKey="img_thumb"
|
||||
:gap="12"
|
||||
:bottomGap="-5"
|
||||
:colWidth="colWidth"
|
||||
:distanceToScroll="100"
|
||||
:isLoading="loading"
|
||||
:isOver="isOver"
|
||||
@scrollReachBottom="getNext"
|
||||
:row-key="waterfallOptions.rowKey"
|
||||
:gutter="waterfallOptions.gutter"
|
||||
:has-around-gutter="waterfallOptions.hasAroundGutter"
|
||||
:width="waterfallOptions.width"
|
||||
:breakpoints="waterfallOptions.breakpoints"
|
||||
:img-selector="waterfallOptions.imgSelector"
|
||||
:background-color="waterfallOptions.backgroundColor"
|
||||
:animation-effect="waterfallOptions.animationEffect"
|
||||
:animation-duration="waterfallOptions.animationDuration"
|
||||
:animation-delay="waterfallOptions.animationDelay"
|
||||
:animation-cancel="waterfallOptions.animationCancel"
|
||||
:lazyload="waterfallOptions.lazyload"
|
||||
:load-props="waterfallOptions.loadProps"
|
||||
:cross-origin="waterfallOptions.crossOrigin"
|
||||
:align="waterfallOptions.align"
|
||||
:is-loading="loading"
|
||||
:is-over="isOver"
|
||||
@afterRender="loading = false"
|
||||
>
|
||||
<template #default="slotProp">
|
||||
<div class="list-item">
|
||||
<div class="image">
|
||||
<el-image :src="slotProp.item['img_thumb']" loading="lazy" @click="showTask(slotProp.item)">
|
||||
<template #placeholder>
|
||||
<div class="image-slot">正在加载图片</div>
|
||||
</template>
|
||||
<template #default="{ item, url }">
|
||||
<div
|
||||
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
|
||||
>
|
||||
<div class="overflow-hidden rounded-lg">
|
||||
<LazyImg
|
||||
:url="url"
|
||||
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
|
||||
@click="showTask(item)"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
|
||||
<div
|
||||
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
|
||||
>
|
||||
<div class="opt">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="复制提示词"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
type="info"
|
||||
circle
|
||||
class="copy-prompt-wall"
|
||||
:data-clipboard-text="item.prompt"
|
||||
>
|
||||
<i class="iconfont icon-file"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="画同款"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
@click="drawSameSd(item)"
|
||||
>
|
||||
<i class="iconfont icon-palette"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v3-waterfall>
|
||||
</Waterfall>
|
||||
|
||||
<div class="footer" v-if="isOver">
|
||||
<!-- <el-empty
|
||||
:image-size="100"
|
||||
:image="nodata"
|
||||
description="没有更多数据了"
|
||||
/> -->
|
||||
<span>没有更多数据了</span>
|
||||
<i class="iconfont icon-face"></i>
|
||||
<Waterfall
|
||||
v-if="imgType === 'dall'"
|
||||
id="waterfall-dall"
|
||||
:list="data['dall']"
|
||||
:row-key="waterfallOptions.rowKey"
|
||||
:gutter="waterfallOptions.gutter"
|
||||
:has-around-gutter="waterfallOptions.hasAroundGutter"
|
||||
:width="waterfallOptions.width"
|
||||
:breakpoints="waterfallOptions.breakpoints"
|
||||
:img-selector="waterfallOptions.imgSelector"
|
||||
:background-color="waterfallOptions.backgroundColor"
|
||||
:animation-effect="waterfallOptions.animationEffect"
|
||||
:animation-duration="waterfallOptions.animationDuration"
|
||||
:animation-delay="waterfallOptions.animationDelay"
|
||||
:animation-cancel="waterfallOptions.animationCancel"
|
||||
:lazyload="waterfallOptions.lazyload"
|
||||
:load-props="waterfallOptions.loadProps"
|
||||
:cross-origin="waterfallOptions.crossOrigin"
|
||||
:align="waterfallOptions.align"
|
||||
:is-loading="loading"
|
||||
:is-over="isOver"
|
||||
@afterRender="loading = false"
|
||||
>
|
||||
<template #default="{ item, url }">
|
||||
<div
|
||||
class="bg-gray-900 rounded-lg shadow-md overflow-hidden transition-all duration-300 ease-linear hover:shadow-md hover:shadow-purple-800 group"
|
||||
>
|
||||
<div class="overflow-hidden rounded-lg">
|
||||
<LazyImg
|
||||
:url="url"
|
||||
class="cursor-pointer transition-all duration-300 ease-linear group-hover:scale-105"
|
||||
@click="previewImg(item)"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-4 pt-2 pb-4 border-t border-t-gray-800">
|
||||
<div
|
||||
class="pt-3 flex justify-center items-center border-t border-t-gray-600 border-opacity-50"
|
||||
>
|
||||
<div class="opt">
|
||||
<el-tooltip
|
||||
class="box-item"
|
||||
content="复制提示词"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
type="info"
|
||||
circle
|
||||
class="copy-prompt-wall"
|
||||
:data-clipboard-text="item.prompt"
|
||||
>
|
||||
<i class="iconfont icon-file"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Waterfall>
|
||||
|
||||
<div class="flex flex-col items-center justify-center py-10">
|
||||
<img
|
||||
:src="waterfallOptions.loadProps.loading"
|
||||
class="max-w-[50px] max-h-[50px]"
|
||||
v-if="loading"
|
||||
/>
|
||||
<div v-else>
|
||||
<button
|
||||
class="px-5 py-2 rounded-full bg-purple-700 text-md text-white cursor-pointer hover:bg-purple-800 transition-all duration-300"
|
||||
@click="getNext"
|
||||
v-if="!isOver"
|
||||
>
|
||||
加载更多
|
||||
</button>
|
||||
<div class="no-more-data" v-else>
|
||||
<span class="text-gray-500 mr-2">没有更多数据了</span>
|
||||
<i class="iconfont icon-face"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<back-top :right="30" :bottom="30" />
|
||||
@@ -161,7 +250,23 @@
|
||||
<!-- end of waterfall -->
|
||||
</div>
|
||||
<!-- 任务详情弹框 -->
|
||||
<sd-task-view v-model="showTaskDialog" :data="item" @drawSame="drawSameSd" @close="showTaskDialog = false" />
|
||||
<sd-task-view
|
||||
v-model="showTaskDialog"
|
||||
:data="item"
|
||||
@drawSame="drawSameSd"
|
||||
@close="showTaskDialog = false"
|
||||
/>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<el-image-viewer
|
||||
@close="
|
||||
() => {
|
||||
previewURL = '';
|
||||
}
|
||||
"
|
||||
v-if="previewURL !== ''"
|
||||
:url-list="[previewURL]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -175,6 +280,13 @@ import Clipboard from "clipboard";
|
||||
import { useRouter } from "vue-router";
|
||||
import BackTop from "@/components/BackTop.vue";
|
||||
import SdTaskView from "@/components/SdTaskView.vue";
|
||||
import { LazyImg, Waterfall } from "vue-waterfall-plugin-next";
|
||||
import "vue-waterfall-plugin-next/dist/style.css";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
|
||||
const store = useSharedStore();
|
||||
const waterfallOptions = store.waterfallOptions;
|
||||
|
||||
const data = ref({
|
||||
mj: [],
|
||||
sd: [],
|
||||
@@ -184,19 +296,12 @@ const loading = ref(true);
|
||||
const isOver = ref(false);
|
||||
const imgType = ref("mj"); // 图片类别
|
||||
const listBoxHeight = window.innerHeight - 124;
|
||||
const colWidth = ref(220);
|
||||
const showTaskDialog = ref(false);
|
||||
const item = ref({});
|
||||
const previewURL = ref("");
|
||||
|
||||
// 计算瀑布流列宽度
|
||||
const calcColWidth = () => {
|
||||
const listBoxWidth = window.innerWidth - 60 - 80;
|
||||
const rows = Math.floor(listBoxWidth / colWidth.value);
|
||||
colWidth.value = Math.floor((listBoxWidth - (rows - 1) * 12) / rows);
|
||||
};
|
||||
calcColWidth();
|
||||
window.onresize = () => {
|
||||
calcColWidth();
|
||||
const previewImg = (item) => {
|
||||
previewURL.value = item.img_url;
|
||||
};
|
||||
|
||||
const page = ref(0);
|
||||
@@ -223,16 +328,17 @@ const getNext = () => {
|
||||
}
|
||||
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (!res.data.items || res.data.items.length === 0) {
|
||||
isOver.value = true;
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成缩略图
|
||||
const imageList = res.data.items;
|
||||
for (let i = 0; i < imageList.length; i++) {
|
||||
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
|
||||
imageList[i]["img_thumb"] =
|
||||
imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75";
|
||||
}
|
||||
if (data.value[imgType.value].length === 0) {
|
||||
data.value[imgType.value] = imageList;
|
||||
@@ -246,6 +352,7 @@ const getNext = () => {
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取图片失败:" + e.message);
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -300,6 +407,6 @@ const drawSameMj = (row) => {
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
@import "@/assets/css/images-wall.styl"
|
||||
@import "@/assets/css/custom-scroll.styl"
|
||||
@import '@/assets/css/images-wall.styl';
|
||||
@import '@/assets/css/custom-scroll.styl';
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
<AccountTop>
|
||||
<template #default>
|
||||
<div class="wechatLog flex-center" v-if="wechatLoginURL !== ''">
|
||||
<a :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)"> <i class="iconfont icon-wechat"></i>使用微信登录 </a>
|
||||
<a :href="wechatLoginURL" @click="setRoute(router.currentRoute.value.path)">
|
||||
<i class="iconfont icon-wechat"></i>使用微信登录
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</AccountTop>
|
||||
@@ -14,18 +16,34 @@
|
||||
<el-form ref="ruleFormRef" :model="ruleForm" :rules="rules">
|
||||
<el-form-item label="" prop="username">
|
||||
<div class="form-title">账号</div>
|
||||
<el-input v-model="ruleForm.username" size="large" placeholder="请输入账号" @keyup="handleKeyup" />
|
||||
<el-input
|
||||
v-model="ruleForm.username"
|
||||
size="large"
|
||||
placeholder="请输入账号"
|
||||
@keyup="handleKeyup"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="" prop="password">
|
||||
<div class="flex-between w100">
|
||||
<div class="form-title">密码</div>
|
||||
<div class="form-forget text-color-primary" @click="router.push('/resetpassword')">忘记密码?</div>
|
||||
<div class="form-forget text-color-primary" @click="router.push('/resetpassword')">
|
||||
忘记密码?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-input size="large" v-model="ruleForm.password" placeholder="请输入密码" show-password autocomplete="off" @keyup="handleKeyup" />
|
||||
<el-input
|
||||
size="large"
|
||||
v-model="ruleForm.password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
autocomplete="off"
|
||||
@keyup="handleKeyup"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="login-btn" size="large" type="primary" @click="login">登录</el-button>
|
||||
<el-button class="login-btn" size="large" type="primary" @click="login"
|
||||
>登录</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@@ -38,96 +56,105 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, reactive } from "vue";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { useRouter } from "vue-router";
|
||||
import AccountBg from "@/components/AccountBg.vue";
|
||||
import { isMobile } from "@/utils/libs";
|
||||
import { checkSession, getLicenseInfo, getSystemInfo } from "@/store/cache";
|
||||
import { setUserToken } from "@/store/session";
|
||||
import { showMessageError } from "@/utils/dialog";
|
||||
import { setRoute } from "@/store/system";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import AccountBg from '@/components/AccountBg.vue'
|
||||
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
|
||||
import { setUserToken } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { setRoute } from '@/store/system'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import AccountTop from "@/components/AccountTop.vue";
|
||||
import Captcha from "@/components/Captcha.vue";
|
||||
import AccountTop from '@/components/AccountTop.vue'
|
||||
import Captcha from '@/components/Captcha.vue'
|
||||
|
||||
const router = useRouter();
|
||||
const title = ref("");
|
||||
const router = useRouter()
|
||||
const title = ref('')
|
||||
|
||||
const logo = ref("");
|
||||
const licenseConfig = ref({});
|
||||
const wechatLoginURL = ref("");
|
||||
const enableVerify = ref(false);
|
||||
const captchaRef = ref(null);
|
||||
const ruleFormRef = ref(null);
|
||||
const logo = ref('')
|
||||
const licenseConfig = ref({})
|
||||
const wechatLoginURL = ref('')
|
||||
const enableVerify = ref(false)
|
||||
const captchaRef = ref(null)
|
||||
const ruleFormRef = ref(null)
|
||||
const ruleForm = reactive({
|
||||
username: process.env.VUE_APP_USER,
|
||||
password: process.env.VUE_APP_PASS,
|
||||
});
|
||||
})
|
||||
const rules = {
|
||||
username: [{ required: true, trigger: "blur", message: "请输入账号" }],
|
||||
password: [{ required: true, trigger: "blur", message: "请输入密码" }],
|
||||
};
|
||||
username: [{ required: true, trigger: 'blur', message: '请输入账号' }],
|
||||
password: [{ required: true, trigger: 'blur', message: '请输入密码' }],
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查URL中是否存在token参数
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const token = urlParams.get('token')
|
||||
if (token) {
|
||||
setUserToken(token)
|
||||
store.setIsLogin(true)
|
||||
router.push('/chat')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取系统配置
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
logo.value = res.data.logo;
|
||||
title.value = res.data.title;
|
||||
enableVerify.value = res.data["enabled_verify"];
|
||||
logo.value = res.data.logo
|
||||
title.value = res.data.title
|
||||
enableVerify.value = res.data['enabled_verify']
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("获取系统配置失败:" + e.message);
|
||||
title.value = "Geek-AI";
|
||||
});
|
||||
showMessageError('获取系统配置失败:' + e.message)
|
||||
title.value = 'Geek-AI'
|
||||
})
|
||||
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
licenseConfig.value = res.data;
|
||||
licenseConfig.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("获取 License 配置:" + e.message);
|
||||
});
|
||||
showMessageError('获取 License 配置:' + e.message)
|
||||
})
|
||||
|
||||
checkSession()
|
||||
.then(() => {
|
||||
router.back();
|
||||
router.back()
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {})
|
||||
|
||||
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`;
|
||||
httpGet("/api/user/clogin?return_url=" + returnURL)
|
||||
const returnURL = `${location.protocol}//${location.host}/login/callback?action=login`
|
||||
httpGet('/api/user/clogin?return_url=' + returnURL)
|
||||
.then((res) => {
|
||||
wechatLoginURL.value = res.data.url;
|
||||
wechatLoginURL.value = res.data.url
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
});
|
||||
console.error(e)
|
||||
})
|
||||
})
|
||||
|
||||
const handleKeyup = (e) => {
|
||||
if (e.key === "Enter") {
|
||||
login();
|
||||
if (e.key === 'Enter') {
|
||||
login()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const login = async function () {
|
||||
await ruleFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (enableVerify.value) {
|
||||
captchaRef.value.loadCaptcha();
|
||||
captchaRef.value.loadCaptcha()
|
||||
} else {
|
||||
doLogin({});
|
||||
doLogin({})
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const store = useSharedStore();
|
||||
const store = useSharedStore()
|
||||
const doLogin = (verifyData) => {
|
||||
httpPost("/api/user/login", {
|
||||
httpPost('/api/user/login', {
|
||||
username: ruleForm.username,
|
||||
password: ruleForm.password,
|
||||
key: verifyData.key,
|
||||
@@ -135,14 +162,14 @@ const doLogin = (verifyData) => {
|
||||
x: verifyData.x,
|
||||
})
|
||||
.then((res) => {
|
||||
setUserToken(res.data.token);
|
||||
store.setIsLogin(true);
|
||||
router.back();
|
||||
setUserToken(res.data.token)
|
||||
store.setIsLogin(true)
|
||||
router.back()
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError("登录失败," + e.message);
|
||||
});
|
||||
};
|
||||
showMessageError('登录失败,' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
@@ -19,11 +19,7 @@
|
||||
|
||||
<div class="param-line">请选择生成思维导图的AI模型</div>
|
||||
<div class="param-line">
|
||||
<el-select
|
||||
v-model="modelID"
|
||||
placeholder="请选择模型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-select v-model="modelID" placeholder="请选择模型" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in models"
|
||||
:key="item.id"
|
||||
@@ -43,9 +39,7 @@
|
||||
|
||||
<div class="text-info">
|
||||
<el-text type="primary"
|
||||
>当前可用算力:<el-text type="warning">{{
|
||||
loginUser.power
|
||||
}}</el-text></el-text
|
||||
>当前可用算力:<el-text type="warning">{{ loginUser.power }}</el-text></el-text
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -115,179 +109,186 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, ref } from "vue";
|
||||
import { Markmap } from "markmap-view";
|
||||
import { Transformer } from "markmap-lib";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Download } from "@element-plus/icons-vue";
|
||||
import { Toolbar } from "markmap-toolbar";
|
||||
import { useSharedStore } from "@/store/sharedata";
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { Markmap } from 'markmap-view'
|
||||
import { Transformer } from 'markmap-lib'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Download } from '@element-plus/icons-vue'
|
||||
import { Toolbar } from 'markmap-toolbar'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
|
||||
const leftBoxHeight = ref(window.innerHeight - 105);
|
||||
const leftBoxHeight = ref(window.innerHeight - 105)
|
||||
//const rightBoxHeight = ref(window.innerHeight - 115);
|
||||
const rightBoxHeight = ref(window.innerHeight);
|
||||
const rightBoxHeight = ref(window.innerHeight)
|
||||
|
||||
const prompt = ref("");
|
||||
const text = ref("");
|
||||
const content = ref(text.value);
|
||||
const html = ref("");
|
||||
const prompt = ref('')
|
||||
const text = ref('')
|
||||
const content = ref(text.value)
|
||||
const html = ref('')
|
||||
|
||||
const isLogin = ref(false);
|
||||
const loginUser = ref({ power: 0 });
|
||||
const transformer = new Transformer();
|
||||
const store = useSharedStore();
|
||||
const loading = ref(false);
|
||||
const isLogin = ref(false)
|
||||
const loginUser = ref({ power: 0 })
|
||||
const transformer = new Transformer()
|
||||
const store = useSharedStore()
|
||||
const loading = ref(false)
|
||||
|
||||
const svgRef = ref(null);
|
||||
const markMap = ref(null);
|
||||
const models = ref([]);
|
||||
const modelID = ref(0);
|
||||
const svgRef = ref(null)
|
||||
const markMap = ref(null)
|
||||
const models = ref([])
|
||||
const modelID = ref(0)
|
||||
const cacheKey = ref('MarkMapCache')
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
text.value = res.data["mark_map_text"];
|
||||
content.value = text.value;
|
||||
initData();
|
||||
nextTick(() => {
|
||||
try {
|
||||
markMap.value = Markmap.create(svgRef.value);
|
||||
const { el } = Toolbar.create(markMap.value);
|
||||
document.getElementById("toolbar").append(el);
|
||||
update();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
onMounted(async () => {
|
||||
const cache = localStorage.getItem(cacheKey.value)
|
||||
if (cache) {
|
||||
text.value = cache
|
||||
} else {
|
||||
const res = await getSystemInfo().catch((e) => {
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
text.value = res.data['mark_map_text']
|
||||
content.value = text.value
|
||||
}
|
||||
|
||||
initData()
|
||||
nextTick(() => {
|
||||
try {
|
||||
markMap.value = Markmap.create(svgRef.value)
|
||||
const { el } = Toolbar.create(markMap.value)
|
||||
document.getElementById('toolbar').append(el)
|
||||
update()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message);
|
||||
});
|
||||
})
|
||||
|
||||
const initData = () => {
|
||||
httpGet("/api/model/list")
|
||||
httpGet('/api/model/list')
|
||||
.then((res) => {
|
||||
for (let v of res.data) {
|
||||
models.value.push(v);
|
||||
models.value.push(v)
|
||||
}
|
||||
modelID.value = models.value[0].id;
|
||||
modelID.value = models.value[0].id
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取模型失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('获取模型失败:' + e.message)
|
||||
})
|
||||
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
loginUser.value = user;
|
||||
isLogin.value = true;
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
try {
|
||||
const { root } = transformer.transform(processContent(text.value));
|
||||
markMap.value.setData(root);
|
||||
markMap.value.fit();
|
||||
const { root } = transformer.transform(processContent(text.value))
|
||||
markMap.value.setData(root)
|
||||
markMap.value.fit()
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.error(e)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const processContent = (text) => {
|
||||
if (!text) {
|
||||
return text;
|
||||
return text
|
||||
}
|
||||
|
||||
const arr = [];
|
||||
const lines = text.split("\n");
|
||||
const arr = []
|
||||
const lines = text.split('\n')
|
||||
for (let line of lines) {
|
||||
if (line.indexOf("```") !== -1) {
|
||||
continue;
|
||||
if (line.indexOf('```') !== -1) {
|
||||
continue
|
||||
}
|
||||
line = line.replace(/([*_~`>])|(\d+\.)\s/g, "");
|
||||
arr.push(line);
|
||||
line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
|
||||
arr.push(line)
|
||||
}
|
||||
return arr.join("\n");
|
||||
};
|
||||
return arr.join('\n')
|
||||
}
|
||||
|
||||
window.onresize = () => {
|
||||
leftBoxHeight.value = window.innerHeight - 145;
|
||||
rightBoxHeight.value = window.innerHeight - 85;
|
||||
};
|
||||
leftBoxHeight.value = window.innerHeight - 145
|
||||
rightBoxHeight.value = window.innerHeight - 85
|
||||
}
|
||||
|
||||
const generate = () => {
|
||||
text.value = content.value;
|
||||
update();
|
||||
};
|
||||
text.value = content.value
|
||||
update()
|
||||
}
|
||||
|
||||
// 使用 AI 智能生成
|
||||
const generateAI = () => {
|
||||
html.value = "";
|
||||
text.value = "";
|
||||
if (prompt.value === "") {
|
||||
return ElMessage.error("请输入你的需求");
|
||||
html.value = ''
|
||||
text.value = ''
|
||||
if (prompt.value === '') {
|
||||
return ElMessage.error('请输入你的需求')
|
||||
}
|
||||
if (!isLogin.value) {
|
||||
store.setShowLoginDialog(true);
|
||||
return;
|
||||
store.setShowLoginDialog(true)
|
||||
return
|
||||
}
|
||||
loading.value = true;
|
||||
httpPost("/api/markMap/gen", {
|
||||
loading.value = true
|
||||
httpPost('/api/markMap/gen', {
|
||||
prompt: prompt.value,
|
||||
model_id: modelID.value
|
||||
model_id: modelID.value,
|
||||
})
|
||||
.then((res) => {
|
||||
text.value = res.data;
|
||||
content.value = processContent(text.value);
|
||||
const model = getModelById(modelID.value);
|
||||
loginUser.value.power -= model.power;
|
||||
nextTick(() => update());
|
||||
loading.value = false;
|
||||
text.value = res.data
|
||||
content.value = processContent(text.value)
|
||||
const model = getModelById(modelID.value)
|
||||
loginUser.value.power -= model.power
|
||||
nextTick(() => update())
|
||||
loading.value = false
|
||||
// 缓存结果
|
||||
localStorage.setItem(cacheKey.value, text.value)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("生成思维导图失败:" + e.message);
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
ElMessage.error('生成思维导图失败:' + e.message)
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const getModelById = (modelId) => {
|
||||
for (let m of models.value) {
|
||||
if (m.id === modelId) {
|
||||
return m;
|
||||
return m
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// download SVG to png file
|
||||
const downloadImage = () => {
|
||||
const svgElement = document.getElementById("markmap");
|
||||
const svgElement = document.getElementById('markmap')
|
||||
// 将 SVG 渲染到图片对象
|
||||
const serializer = new XMLSerializer();
|
||||
const serializer = new XMLSerializer()
|
||||
const source =
|
||||
'<?xml version="1.0" standalone="no"?>\r\n' +
|
||||
serializer.serializeToString(svgRef.value);
|
||||
const image = new Image();
|
||||
image.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
|
||||
'<?xml version="1.0" standalone="no"?>\r\n' + serializer.serializeToString(svgRef.value)
|
||||
const image = new Image()
|
||||
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source)
|
||||
|
||||
// 将图片对象渲染
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = svgElement.offsetWidth;
|
||||
canvas.height = svgElement.offsetHeight;
|
||||
let context = canvas.getContext("2d");
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
context.fillStyle = "white";
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = svgElement.offsetWidth
|
||||
canvas.height = svgElement.offsetHeight
|
||||
let context = canvas.getContext('2d')
|
||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||
context.fillStyle = 'white'
|
||||
context.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
image.onload = function () {
|
||||
context.drawImage(image, 0, 0);
|
||||
const a = document.createElement("a");
|
||||
a.download = "geek-ai-xmind.png";
|
||||
a.href = canvas.toDataURL(`image/png`);
|
||||
a.click();
|
||||
};
|
||||
};
|
||||
context.drawImage(image, 0, 0)
|
||||
const a = document.createElement('a')
|
||||
a.download = 'geek-ai-xmind.png'
|
||||
a.href = canvas.toDataURL(`image/png`)
|
||||
a.click()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
<template>
|
||||
<div data-component="ConsolePage">
|
||||
<div class="content-top">
|
||||
<div class="content-title">
|
||||
<img src="/openai-logomark.svg" alt="OpenAI Logo" />
|
||||
<span>realtime console</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="content-main">
|
||||
<div class="content-logs">
|
||||
<div class="content-block events">
|
||||
<div class="visualization">
|
||||
<div class="visualization-entry client">
|
||||
<canvas ref="clientCanvasRef" />
|
||||
</div>
|
||||
<div class="visualization-entry server">
|
||||
<canvas ref="serverCanvasRef" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-block-title">events</div>
|
||||
<div class="content-block-body" ref="eventsScrollRef">
|
||||
<template v-if="!realtimeEvents.length">
|
||||
awaiting connection...
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="(realtimeEvent, i) in realtimeEvents" :key="realtimeEvent.event.event_id" class="event">
|
||||
<div class="event-timestamp">
|
||||
{{ formatTime(realtimeEvent.time) }}
|
||||
</div>
|
||||
<div class="event-details">
|
||||
<div
|
||||
class="event-summary"
|
||||
@click="toggleEventDetails(realtimeEvent.event.event_id)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'event-source',
|
||||
realtimeEvent.event.type === 'error'
|
||||
? 'error'
|
||||
: realtimeEvent.source,
|
||||
]"
|
||||
>
|
||||
<component :is="realtimeEvent.source === 'client' ? ArrowUp : ArrowDown" />
|
||||
<span>
|
||||
{{ realtimeEvent.event.type === 'error'
|
||||
? 'error!'
|
||||
: realtimeEvent.source }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="event-type">
|
||||
{{ realtimeEvent.event.type }}
|
||||
{{ realtimeEvent.count ? `(${realtimeEvent.count})` : '' }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expandedEvents[realtimeEvent.event.event_id]"
|
||||
class="event-payload"
|
||||
>
|
||||
{{ JSON.stringify(realtimeEvent.event, null, 2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-block conversation">
|
||||
<div class="content-block-title">conversation</div>
|
||||
<div class="content-block-body" data-conversation-content>
|
||||
<template v-if="!items.length">
|
||||
awaiting connection...
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(conversationItem, i) in items"
|
||||
:key="conversationItem.id"
|
||||
class="conversation-item"
|
||||
>
|
||||
<div :class="['speaker', conversationItem.role || '']">
|
||||
<div>
|
||||
{{
|
||||
(conversationItem.role || conversationItem.type).replaceAll(
|
||||
'_',
|
||||
' '
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div class="close" @click="deleteConversationItem(conversationItem.id)">
|
||||
<X />
|
||||
</div>
|
||||
</div>
|
||||
<div class="speaker-content">
|
||||
<!-- tool response -->
|
||||
<div v-if="conversationItem.type === 'function_call_output'">
|
||||
{{ conversationItem.formatted.output }}
|
||||
</div>
|
||||
<!-- tool call -->
|
||||
<div v-if="conversationItem.formatted.tool">
|
||||
{{ conversationItem.formatted.tool.name }}(
|
||||
{{ conversationItem.formatted.tool.arguments }})
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
!conversationItem.formatted.tool &&
|
||||
conversationItem.role === 'user'
|
||||
"
|
||||
>
|
||||
{{
|
||||
conversationItem.formatted.transcript ||
|
||||
(conversationItem.formatted.audio?.length
|
||||
? '(awaiting transcript)'
|
||||
: conversationItem.formatted.text || '(item sent)')
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
!conversationItem.formatted.tool &&
|
||||
conversationItem.role === 'assistant'
|
||||
"
|
||||
>
|
||||
{{
|
||||
conversationItem.formatted.transcript ||
|
||||
conversationItem.formatted.text ||
|
||||
'(truncated)'
|
||||
}}
|
||||
</div>
|
||||
<audio
|
||||
v-if="conversationItem.formatted.file"
|
||||
:src="conversationItem.formatted.file.url"
|
||||
controls
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-actions" style="position:absolute; top: 0; left: 0">
|
||||
<el-button
|
||||
:type="isConnected ? '' : 'primary'"
|
||||
@click="connectConversation"
|
||||
>
|
||||
{{isConnected ? '断开连接' : '连接对话'}}
|
||||
</el-button>
|
||||
|
||||
<el-button @mousedown="startRecording" @mouseup="stopRecording">开始讲话</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { RealtimeClient } from '@openai/realtime-api-beta';
|
||||
import { WavRecorder, WavStreamPlayer } from '@/lib/wavtools/index.js';
|
||||
import { instructions } from '@/utils/conversation_config.js';
|
||||
import { WavRenderer } from '@/utils/wav_renderer';
|
||||
|
||||
// Constants
|
||||
const LOCAL_RELAY_SERVER_URL = process.env.REACT_APP_LOCAL_RELAY_SERVER_URL || '';
|
||||
|
||||
// Reactive state
|
||||
const apiKey = ref(
|
||||
LOCAL_RELAY_SERVER_URL
|
||||
? ''
|
||||
: localStorage.getItem('tmp::voice_api_key') || prompt('OpenAI API Key') || ''
|
||||
);
|
||||
const wavRecorder = ref(new WavRecorder({ sampleRate: 24000 }));
|
||||
const wavStreamPlayer = ref(new WavStreamPlayer({ sampleRate: 24000 }));
|
||||
const client = ref(
|
||||
new RealtimeClient({
|
||||
url: "ws://localhost:5678/api/realtime",
|
||||
apiKey: "sk-Gc5cEzDzGQLIqxWA9d62089350F3454bB359C4A3Fa21B3E4",
|
||||
dangerouslyAllowAPIKeyInBrowser: true,
|
||||
})
|
||||
);
|
||||
|
||||
const clientCanvasRef = ref(null);
|
||||
const serverCanvasRef = ref(null);
|
||||
const eventsScrollRef = ref(null);
|
||||
const startTime = ref(new Date().toISOString());
|
||||
|
||||
const items = ref([]);
|
||||
const realtimeEvents = ref([]);
|
||||
const expandedEvents = reactive({});
|
||||
const isConnected = ref(false);
|
||||
const canPushToTalk = ref(true);
|
||||
const isRecording = ref(false);
|
||||
const memoryKv = ref({});
|
||||
const coords = ref({ lat: 37.775593, lng: -122.418137 });
|
||||
const marker = ref(null);
|
||||
|
||||
// Methods
|
||||
const formatTime = (timestamp) => {
|
||||
const t0 = new Date(startTime.value).valueOf();
|
||||
const t1 = new Date(timestamp).valueOf();
|
||||
const delta = t1 - t0;
|
||||
const hs = Math.floor(delta / 10) % 100;
|
||||
const s = Math.floor(delta / 1000) % 60;
|
||||
const m = Math.floor(delta / 60_000) % 60;
|
||||
const pad = (n) => {
|
||||
let s = n + '';
|
||||
while (s.length < 2) {
|
||||
s = '0' + s;
|
||||
}
|
||||
return s;
|
||||
};
|
||||
return `${pad(m)}:${pad(s)}.${pad(hs)}`;
|
||||
};
|
||||
|
||||
const connectConversation = async () => {
|
||||
alert(123)
|
||||
startTime.value = new Date().toISOString();
|
||||
isConnected.value = true;
|
||||
realtimeEvents.value = [];
|
||||
items.value = client.value.conversation.getItems();
|
||||
|
||||
await wavRecorder.value.begin();
|
||||
await wavStreamPlayer.value.connect();
|
||||
await client.value.connect();
|
||||
client.value.sendUserMessageContent([
|
||||
{
|
||||
type: 'input_text',
|
||||
text: '你好,我是老阳!',
|
||||
},
|
||||
]);
|
||||
|
||||
if (client.value.getTurnDetectionType() === 'server_vad') {
|
||||
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono));
|
||||
}
|
||||
};
|
||||
|
||||
const disconnectConversation = async () => {
|
||||
isConnected.value = false;
|
||||
realtimeEvents.value = [];
|
||||
items.value = [];
|
||||
memoryKv.value = {};
|
||||
coords.value = { lat: 37.775593, lng: -122.418137 };
|
||||
marker.value = null;
|
||||
|
||||
client.value.disconnect();
|
||||
await wavRecorder.value.end();
|
||||
await wavStreamPlayer.value.interrupt();
|
||||
};
|
||||
|
||||
const deleteConversationItem = async (id) => {
|
||||
client.value.deleteItem(id);
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
isRecording.value = true;
|
||||
const trackSampleOffset = await wavStreamPlayer.value.interrupt();
|
||||
if (trackSampleOffset?.trackId) {
|
||||
const { trackId, offset } = trackSampleOffset;
|
||||
await client.value.cancelResponse(trackId, offset);
|
||||
}
|
||||
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono));
|
||||
};
|
||||
|
||||
const stopRecording = async () => {
|
||||
isRecording.value = false;
|
||||
await wavRecorder.value.pause();
|
||||
client.value.createResponse();
|
||||
};
|
||||
|
||||
const changeTurnEndType = async (value) => {
|
||||
if (value === 'none' && wavRecorder.value.getStatus() === 'recording') {
|
||||
await wavRecorder.value.pause();
|
||||
}
|
||||
client.value.updateSession({
|
||||
turn_detection: value === 'none' ? null : { type: 'server_vad' },
|
||||
});
|
||||
if (value === 'server_vad' && client.value.isConnected()) {
|
||||
await wavRecorder.value.record((data) => client.value.appendInputAudio(data.mono));
|
||||
}
|
||||
canPushToTalk.value = value === 'none';
|
||||
};
|
||||
|
||||
const toggleEventDetails = (eventId) => {
|
||||
if (expandedEvents[eventId]) {
|
||||
delete expandedEvents[eventId];
|
||||
} else {
|
||||
expandedEvents[eventId] = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Lifecycle hooks and watchers
|
||||
onMounted(() => {
|
||||
if (apiKey.value !== '') {
|
||||
localStorage.setItem('tmp::voice_api_key', apiKey.value);
|
||||
}
|
||||
|
||||
// Set up render loops for the visualization canvas
|
||||
let isLoaded = true;
|
||||
const render = () => {
|
||||
if (isLoaded) {
|
||||
if (clientCanvasRef.value) {
|
||||
const canvas = clientCanvasRef.value;
|
||||
if (!canvas.width || !canvas.height) {
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
}
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
const result = wavRecorder.value.recording
|
||||
? wavRecorder.value.getFrequencies('voice')
|
||||
: { values: new Float32Array([0]) };
|
||||
WavRenderer.drawBars(canvas, ctx, result.values, '#0099ff', 10, 0, 8);
|
||||
}
|
||||
}
|
||||
if (serverCanvasRef.value) {
|
||||
const canvas = serverCanvasRef.value;
|
||||
if (!canvas.width || !canvas.height) {
|
||||
canvas.width = canvas.offsetWidth;
|
||||
canvas.height = canvas.offsetHeight;
|
||||
}
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
const result = wavStreamPlayer.value.analyser
|
||||
? wavStreamPlayer.value.getFrequencies('voice')
|
||||
: { values: new Float32Array([0]) };
|
||||
WavRenderer.drawBars(canvas, ctx, result.values, '#009900', 10, 0, 8);
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
};
|
||||
render();
|
||||
|
||||
// Set up client event listeners
|
||||
client.value.on('realtime.event', (realtimeEvent) => {
|
||||
realtimeEvents.value = realtimeEvents.value.slice();
|
||||
const lastEvent = realtimeEvents.value[realtimeEvents.value.length - 1];
|
||||
if (lastEvent?.event.type === realtimeEvent.event.type) {
|
||||
lastEvent.count = (lastEvent.count || 0) + 1;
|
||||
realtimeEvents.value.splice(-1, 1, lastEvent);
|
||||
} else {
|
||||
realtimeEvents.value.push(realtimeEvent);
|
||||
}
|
||||
});
|
||||
|
||||
client.value.on('error', (event) => console.error(event));
|
||||
|
||||
client.value.on('conversation.interrupted', async () => {
|
||||
const trackSampleOffset = await wavStreamPlayer.value.interrupt();
|
||||
if (trackSampleOffset?.trackId) {
|
||||
const { trackId, offset } = trackSampleOffset;
|
||||
await client.value.cancelResponse(trackId, offset);
|
||||
}
|
||||
});
|
||||
|
||||
client.value.on('conversation.updated', async ({ item, delta }) => {
|
||||
items.value = client.value.conversation.getItems();
|
||||
if (delta?.audio) {
|
||||
wavStreamPlayer.value.add16BitPCM(delta.audio, item.id);
|
||||
}
|
||||
if (item.status === 'completed' && item.formatted.audio?.length) {
|
||||
const wavFile = await WavRecorder.decode(
|
||||
item.formatted.audio,
|
||||
24000,
|
||||
24000
|
||||
);
|
||||
item.formatted.file = wavFile;
|
||||
}
|
||||
});
|
||||
|
||||
// Set up client instructions and tools
|
||||
client.value.updateSession({ instructions: instructions });
|
||||
client.value.updateSession({ input_audio_transcription: { model: 'whisper-1' } });
|
||||
|
||||
client.value.addTool(
|
||||
{
|
||||
name: 'set_memory',
|
||||
description: 'Saves important data about the user into memory.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
key: {
|
||||
type: 'string',
|
||||
description:
|
||||
'The key of the memory value. Always use lowercase and underscores, no other characters.',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'Value can be anything represented as a string',
|
||||
},
|
||||
},
|
||||
required: ['key', 'value'],
|
||||
},
|
||||
},
|
||||
async ({ key, value }) => {
|
||||
memoryKv.value = { ...memoryKv.value, [key]: value };
|
||||
return { ok: true };
|
||||
}
|
||||
);
|
||||
|
||||
client.value.addTool(
|
||||
{
|
||||
name: 'get_weather',
|
||||
description:
|
||||
'Retrieves the weather for a given lat, lng coordinate pair. Specify a label for the location.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
lat: {
|
||||
type: 'number',
|
||||
description: 'Latitude',
|
||||
},
|
||||
lng: {
|
||||
type: 'number',
|
||||
description: 'Longitude',
|
||||
},
|
||||
location: {
|
||||
type: 'string',
|
||||
description: 'Name of the location',
|
||||
},
|
||||
},
|
||||
required: ['lat', 'lng', 'location'],
|
||||
},
|
||||
},
|
||||
async ({ lat, lng, location }) => {
|
||||
marker.value = { lat, lng, location };
|
||||
coords.value = { lat, lng, location };
|
||||
const result = await fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}¤t=temperature_2m,wind_speed_10m`
|
||||
);
|
||||
const json = await result.json();
|
||||
const temperature = {
|
||||
value: json.current.temperature_2m,
|
||||
units: json.current_units.temperature_2m,
|
||||
};
|
||||
const wind_speed = {
|
||||
value: json.current.wind_speed_10m,
|
||||
units: json.current_units.wind_speed_10m,
|
||||
};
|
||||
marker.value = { lat, lng, location, temperature, wind_speed };
|
||||
return json;
|
||||
}
|
||||
);
|
||||
|
||||
items.value = client.value.conversation.getItems();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
client.value.reset();
|
||||
});
|
||||
|
||||
// Watchers
|
||||
watch(realtimeEvents, () => {
|
||||
if (eventsScrollRef.value) {
|
||||
const eventsEl = eventsScrollRef.value;
|
||||
eventsEl.scrollTop = eventsEl.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
watch(items, () => {
|
||||
const conversationEls = document.querySelectorAll('[data-conversation-content]');
|
||||
conversationEls.forEach((el) => {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* You can add your component-specific styles here */
|
||||
/* If you're using SCSS, you might want to import your existing SCSS file */
|
||||
/* @import './ConsolePage.scss'; */
|
||||
</style>
|
||||
@@ -142,6 +142,7 @@ const types = ref([
|
||||
{ label: "Luma视频", value: "luma" },
|
||||
{ label: "可灵视频", value: "keling" },
|
||||
{ label: "Realtime API", value: "realtime" },
|
||||
{ label: "语音合成", value: "tts" },
|
||||
{ label: "其他", value: "other" },
|
||||
]);
|
||||
const isEdit = ref(false);
|
||||
|
||||
@@ -126,6 +126,16 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div v-if="item.type === 'tts'">
|
||||
<el-form-item label="音色" prop="voice">
|
||||
<el-select v-model="item.options.voice" placeholder="请选择音色">
|
||||
<el-option v-for="v in voices" :value="v.value" :label="v.label" :key="v.value">
|
||||
{{ v.label }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item label="绑定API-KEY:" prop="apikey">
|
||||
<el-select v-model="item.key_id" placeholder="请选择 API KEY" filterable clearable>
|
||||
<el-option v-for="v in apiKeys" :value="v.id" :label="v.name" :key="v.id">
|
||||
@@ -191,6 +201,15 @@ const formRef = ref(null);
|
||||
const type = ref([
|
||||
{ label: "聊天", value: "chat" },
|
||||
{ label: "绘图", value: "img" },
|
||||
{ label: "语音", value: "tts" },
|
||||
]);
|
||||
|
||||
const voices = ref([
|
||||
{ label: "Echo", value: "echo" },
|
||||
{ label: "Fable", value: "fable" },
|
||||
{ label: "Onyx", value: "onyx" },
|
||||
{ label: "Nova", value: "nova" },
|
||||
{ label: "Shimmer", value: "shimmer" },
|
||||
]);
|
||||
|
||||
// 获取 API KEY
|
||||
@@ -270,7 +289,7 @@ onUnmounted(() => {
|
||||
const add = function () {
|
||||
title.value = "新增模型";
|
||||
showDialog.value = true;
|
||||
item.value = { enabled: true, power: 1, open: true, max_tokens: 1024, max_context: 8192, temperature: 0.9 };
|
||||
item.value = { enabled: true, power: 1, open: true, max_tokens: 1024, max_context: 8192, temperature: 0.9, options: {} };
|
||||
};
|
||||
|
||||
const edit = function (row) {
|
||||
@@ -282,9 +301,6 @@ const edit = function (row) {
|
||||
const save = function () {
|
||||
formRef.value.validate((valid) => {
|
||||
item.value.temperature = parseFloat(item.value.temperature);
|
||||
if (!item.value.sort_num) {
|
||||
item.value.sort_num = items.value.length;
|
||||
}
|
||||
if (valid) {
|
||||
showDialog.value = false;
|
||||
item.value.key_id = parseInt(item.value.key_id);
|
||||
|
||||
@@ -10,13 +10,24 @@
|
||||
</div>
|
||||
|
||||
<el-row>
|
||||
<el-table :data="users.items" border class="table" :row-key="(row) => row.id" @selection-change="handleSelectionChange" table-layout="auto">
|
||||
<el-table
|
||||
:data="users.items"
|
||||
border
|
||||
class="table"
|
||||
:row-key="(row) => row.id"
|
||||
@selection-change="handleSelectionChange"
|
||||
table-layout="auto"
|
||||
>
|
||||
<el-table-column type="selection" width="38"></el-table-column>
|
||||
<el-table-column prop="id" label="ID" />
|
||||
<el-table-column label="账号">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.username }}</span>
|
||||
<el-image v-if="scope.row.vip" :src="vipImg" style="height: 20px; position: relative; top: 5px; left: 5px" />
|
||||
<el-image
|
||||
v-if="scope.row.vip"
|
||||
:src="vipImg"
|
||||
style="height: 20px; position: relative; top: 5px; left: 5px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="mobile" label="手机" />
|
||||
@@ -31,24 +42,42 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row['expired_time']">{{ scope.row["expired_time"] }}</span>
|
||||
<span v-if="scope.row['expired_time']">{{ scope.row['expired_time'] }}</span>
|
||||
<el-tag v-else>长期有效</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="注册时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column fixed="right" label="操作" width="220">
|
||||
<el-table-column fixed="right" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button-group class="ml-4">
|
||||
<el-button size="small" type="primary" @click="userEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="removeUser(scope.row)">删除</el-button>
|
||||
<el-button size="small" type="success" @click="resetPass(scope.row)">重置密码</el-button>
|
||||
</el-button-group>
|
||||
<el-dropdown>
|
||||
<button
|
||||
class="px-2 py-1 bg-purple-200 hover:bg-purple-300 rounded text-purple-800 text-sm"
|
||||
>
|
||||
<i class="iconfont icon-more-horizontal"></i>
|
||||
</button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="userEdit(scope.row)">
|
||||
<i class="iconfont icon-edit mr-1"></i>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="removeUser(scope.row)" class="text-red-500">
|
||||
<i class="iconfont icon-remove mr-1"></i>删除
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="resetPass(scope.row)">
|
||||
<i class="iconfont icon-password mr-1"></i>重置密码
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="genLoginLink(scope.row)">
|
||||
<i class="iconfont icon-link mr-1"></i>生成登录链接
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -66,7 +95,12 @@
|
||||
</div>
|
||||
</el-row>
|
||||
|
||||
<el-dialog v-model="showUserEditDialog" :title="title" :close-on-click-modal="false" width="50%">
|
||||
<el-dialog
|
||||
v-model="showUserEditDialog"
|
||||
:title="title"
|
||||
:close-on-click-modal="false"
|
||||
width="50%"
|
||||
>
|
||||
<el-form :model="user" label-width="100px" ref="userEditFormRef" :rules="rules">
|
||||
<el-form-item label="账号:" prop="username">
|
||||
<el-input v-model="user.username" autocomplete="off" />
|
||||
@@ -96,13 +130,23 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="聊天角色" prop="chat_roles">
|
||||
<el-select v-model="user.chat_roles" multiple :filterable="true" placeholder="选择聊天角色,多选">
|
||||
<el-select
|
||||
v-model="user.chat_roles"
|
||||
multiple
|
||||
:filterable="true"
|
||||
placeholder="选择聊天角色,多选"
|
||||
>
|
||||
<el-option v-for="item in roles" :key="item.key" :label="item.name" :value="item.key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模型权限" prop="chat_models">
|
||||
<el-select v-model="user.chat_models" multiple :filterable="true" placeholder="选择AI模型,多选">
|
||||
<el-select
|
||||
v-model="user.chat_models"
|
||||
multiple
|
||||
:filterable="true"
|
||||
placeholder="选择AI模型,多选"
|
||||
>
|
||||
<el-option v-for="item in models" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -141,204 +185,241 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showGenLoginLinkDialog" title="自动登录链接" width="50%">
|
||||
<el-input v-model="loginLink" readonly disabled />
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button type="primary" @click="copyLoginLink">复制链接</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { dateFormat, disabledDate } from "@/utils/libs";
|
||||
import { Delete, Plus, Search } from "@element-plus/icons-vue";
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { dateFormat, disabledDate } from '@/utils/libs'
|
||||
import { Delete, Plus, Search } from '@element-plus/icons-vue'
|
||||
import { showLoading, closeLoading, showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
|
||||
// 变量定义
|
||||
const users = ref({ page: 1, page_size: 15, items: [] });
|
||||
const query = ref({ username: "", page: 1, page_size: 15 });
|
||||
const users = ref({ page: 1, page_size: 15, items: [] })
|
||||
const query = ref({ username: '', page: 1, page_size: 15 })
|
||||
|
||||
const title = ref("添加用户");
|
||||
const vipImg = ref("/images/menu/member.png");
|
||||
const add = ref(true);
|
||||
const user = ref({ chat_roles: [], chat_models: [] });
|
||||
const pass = ref({ username: "", password: "", id: 0 });
|
||||
const roles = ref([]);
|
||||
const models = ref([]);
|
||||
const showUserEditDialog = ref(false);
|
||||
const showResetPassDialog = ref(false);
|
||||
const title = ref('添加用户')
|
||||
const vipImg = ref('/images/menu/member.png')
|
||||
const add = ref(true)
|
||||
const user = ref({ chat_roles: [], chat_models: [] })
|
||||
const pass = ref({ username: '', password: '', id: 0 })
|
||||
const roles = ref([])
|
||||
const models = ref([])
|
||||
const showUserEditDialog = ref(false)
|
||||
const showResetPassDialog = ref(false)
|
||||
const rules = reactive({
|
||||
username: [{ required: true, message: "请输入账号", trigger: "blur" }],
|
||||
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
||||
password: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value) => {
|
||||
return !(value.length > 16 || value.length < 8);
|
||||
return !(value.length > 16 || value.length < 8)
|
||||
},
|
||||
message: "密码必须为8-16",
|
||||
trigger: "blur",
|
||||
message: '密码必须为8-16',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
calls: [
|
||||
{ required: true, message: "请输入提问次数" },
|
||||
{ type: "number", message: "请输入有效数字" },
|
||||
{ required: true, message: '请输入提问次数' },
|
||||
{ type: 'number', message: '请输入有效数字' },
|
||||
],
|
||||
chat_roles: [{ required: true, message: "请选择聊天角色", trigger: "change" }],
|
||||
chat_models: [{ required: true, message: "请选择AI模型", trigger: "change" }],
|
||||
});
|
||||
const loading = ref(true);
|
||||
chat_roles: [{ required: true, message: '请选择聊天角色', trigger: 'change' }],
|
||||
chat_models: [{ required: true, message: '请选择AI模型', trigger: 'change' }],
|
||||
})
|
||||
const loading = ref(true)
|
||||
|
||||
const userEditFormRef = ref(null);
|
||||
const userEditFormRef = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
fetchUserList(users.value.page, users.value.page_size);
|
||||
fetchUserList(users.value.page, users.value.page_size)
|
||||
// 获取角色列表
|
||||
httpGet("/api/admin/role/list")
|
||||
httpGet('/api/admin/role/list')
|
||||
.then((res) => {
|
||||
roles.value = res.data;
|
||||
roles.value = res.data
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("获取聊天角色失败");
|
||||
});
|
||||
ElMessage.error('获取聊天角色失败')
|
||||
})
|
||||
|
||||
httpGet("/api/admin/model/list")
|
||||
httpGet('/api/admin/model/list')
|
||||
.then((res) => {
|
||||
models.value = res.data;
|
||||
models.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取模型失败:" + e.message);
|
||||
});
|
||||
});
|
||||
ElMessage.error('获取模型失败:' + e.message)
|
||||
})
|
||||
})
|
||||
|
||||
const fetchUserList = function (page, pageSize) {
|
||||
query.value.page = page;
|
||||
query.value.page_size = pageSize;
|
||||
httpGet("/api/admin/user/list", query.value)
|
||||
query.value.page = page
|
||||
query.value.page_size = pageSize
|
||||
httpGet('/api/admin/user/list', query.value)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
// 初始化数据
|
||||
const arr = res.data.items;
|
||||
const arr = res.data.items
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
arr[i].expired_time = dateFormat(arr[i].expired_time);
|
||||
arr[i].expired_time = dateFormat(arr[i].expired_time)
|
||||
}
|
||||
users.value.items = arr;
|
||||
users.value.total = res.data.total;
|
||||
users.value.page = res.data.page;
|
||||
user.value.page_size = res.data.page_size;
|
||||
users.value.items = arr
|
||||
users.value.total = res.data.total
|
||||
users.value.page = res.data.page
|
||||
user.value.page_size = res.data.page_size
|
||||
}
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("加载用户列表失败");
|
||||
});
|
||||
};
|
||||
ElMessage.error('加载用户列表失败')
|
||||
})
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchUserList(users.value.page, users.value.page_size);
|
||||
};
|
||||
fetchUserList(users.value.page, users.value.page_size)
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const removeUser = function (user) {
|
||||
ElMessageBox.confirm("此操作将会永久删除用户信息和聊天记录,确认操作吗?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
ElMessageBox.confirm('此操作将会永久删除用户信息和聊天记录,确认操作吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
httpGet("/api/admin/user/remove", { id: user.id })
|
||||
httpGet('/api/admin/user/remove', { id: user.id })
|
||||
.then(() => {
|
||||
ElMessage.success("操作成功!");
|
||||
fetchUserList(users.value.page, users.value.page_size);
|
||||
ElMessage.success('操作成功!')
|
||||
fetchUserList(users.value.page, users.value.page_size)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("操作失败," + e.message);
|
||||
});
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info("操作被取消");
|
||||
});
|
||||
};
|
||||
ElMessage.info('操作被取消')
|
||||
})
|
||||
}
|
||||
|
||||
const userEdit = function (row) {
|
||||
user.value = row;
|
||||
title.value = "编辑用户";
|
||||
showUserEditDialog.value = true;
|
||||
add.value = false;
|
||||
};
|
||||
user.value = row
|
||||
title.value = '编辑用户'
|
||||
showUserEditDialog.value = true
|
||||
add.value = false
|
||||
}
|
||||
|
||||
const addUser = () => {
|
||||
user.value = { chat_id: 0, chat_roles: [], chat_models: [] };
|
||||
title.value = "添加用户";
|
||||
showUserEditDialog.value = true;
|
||||
add.value = true;
|
||||
};
|
||||
user.value = { chat_id: 0, chat_roles: [], chat_models: [] }
|
||||
title.value = '添加用户'
|
||||
showUserEditDialog.value = true
|
||||
add.value = true
|
||||
}
|
||||
|
||||
const saveUser = function () {
|
||||
userEditFormRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
showUserEditDialog.value = false;
|
||||
console.log(user.value);
|
||||
httpPost("/api/admin/user/save", user.value)
|
||||
showUserEditDialog.value = false
|
||||
console.log(user.value)
|
||||
httpPost('/api/admin/user/save', user.value)
|
||||
.then((res) => {
|
||||
ElMessage.success("操作成功!");
|
||||
ElMessage.success('操作成功!')
|
||||
if (add.value) {
|
||||
users.value.items.push(res.data);
|
||||
users.value.items.push(res.data)
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("操作失败," + e.message);
|
||||
});
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
} else {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
const userIds = ref([]);
|
||||
const userIds = ref([])
|
||||
const handleSelectionChange = function (rows) {
|
||||
userIds.value = [];
|
||||
userIds.value = []
|
||||
rows.forEach((row) => {
|
||||
userIds.value.push(row.id);
|
||||
});
|
||||
};
|
||||
userIds.value.push(row.id)
|
||||
})
|
||||
}
|
||||
|
||||
const multipleDelete = function () {
|
||||
ElMessageBox.confirm("此操作将会永久删除用户信息和聊天记录,确认操作吗?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
ElMessageBox.confirm('此操作将会永久删除用户信息和聊天记录,确认操作吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
httpGet("/api/admin/user/remove", { ids: userIds.value })
|
||||
loading.value = true
|
||||
httpGet('/api/admin/user/remove', { ids: userIds.value })
|
||||
.then(() => {
|
||||
ElMessage.success("操作成功!");
|
||||
fetchUserList(users.value.page, users.value.page_size);
|
||||
loading.value = false;
|
||||
ElMessage.success('操作成功!')
|
||||
fetchUserList(users.value.page, users.value.page_size)
|
||||
loading.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("操作失败," + e.message);
|
||||
loading.value = false;
|
||||
});
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
loading.value = false
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info("操作被取消");
|
||||
});
|
||||
};
|
||||
ElMessage.info('操作被取消')
|
||||
})
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
const resetPass = (row) => {
|
||||
showResetPassDialog.value = true;
|
||||
pass.value.id = row.id;
|
||||
pass.value.username = row.username;
|
||||
};
|
||||
showResetPassDialog.value = true
|
||||
pass.value.id = row.id
|
||||
pass.value.username = row.username
|
||||
}
|
||||
|
||||
const doResetPass = () => {
|
||||
httpPost("/api/admin/user/resetPass", pass.value)
|
||||
httpPost('/api/admin/user/resetPass', pass.value)
|
||||
.then(() => {
|
||||
ElMessage.success("操作成功!");
|
||||
showResetPassDialog.value = false;
|
||||
ElMessage.success('操作成功!')
|
||||
showResetPassDialog.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("操作失败," + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
// 生成登录链接
|
||||
const showGenLoginLinkDialog = ref(false)
|
||||
const loginLink = ref('')
|
||||
const genLoginLink = (row) => {
|
||||
showLoading()
|
||||
httpGet('/api/admin/user/genLoginLink', { id: row.id })
|
||||
.then((res) => {
|
||||
loginLink.value = `${window.location.origin}/login?token=${res.data}`
|
||||
showGenLoginLinkDialog.value = true
|
||||
closeLoading()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('操作失败,' + e.message)
|
||||
closeLoading()
|
||||
})
|
||||
}
|
||||
|
||||
const copyLoginLink = () => {
|
||||
try {
|
||||
navigator.clipboard.writeText(loginLink.value)
|
||||
showMessageOK('复制成功!')
|
||||
} catch (e) {
|
||||
showMessageError('复制失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
|
||||
@@ -3,8 +3,18 @@
|
||||
<el-tabs v-model="activeName" @tab-change="handleChange">
|
||||
<el-tab-pane label="对话列表" name="chat" v-loading="data.chat.loading">
|
||||
<div class="handle-box">
|
||||
<el-input v-model.number="data.chat.query.user_id" placeholder="账户ID" class="handle-input mr10" @keyup="searchChat($event)"></el-input>
|
||||
<el-input v-model="data.chat.query.title" placeholder="对话标题" class="handle-input mr10" @keyup="searchChat($event)"></el-input>
|
||||
<el-input
|
||||
v-model.number="data.chat.query.user_id"
|
||||
placeholder="账户ID"
|
||||
class="handle-input mr10"
|
||||
@keyup="searchChat($event)"
|
||||
></el-input>
|
||||
<el-input
|
||||
v-model="data.chat.query.title"
|
||||
placeholder="对话标题"
|
||||
class="handle-input mr10"
|
||||
@keyup="searchChat($event)"
|
||||
></el-input>
|
||||
<el-date-picker
|
||||
v-model="data.chat.query.created_at"
|
||||
type="daterange"
|
||||
@@ -38,13 +48,15 @@
|
||||
|
||||
<el-table-column label="创建时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="showMessages(scope.row)">查看</el-button>
|
||||
<el-button size="small" type="primary" @click="showMessages(scope.row)"
|
||||
>查看</el-button
|
||||
>
|
||||
<el-popconfirm title="确定要删除当前记录吗?" @confirm="removeChat(scope.row)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
@@ -70,9 +82,24 @@
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="消息记录" name="message">
|
||||
<div class="handle-box">
|
||||
<el-input v-model.number="data.message.query.user_id" placeholder="账户ID" class="handle-input mr10" @keyup="searchMessage($event)"></el-input>
|
||||
<el-input v-model="data.message.query.content" placeholder="消息内容" class="handle-input mr10" @keyup="searchMessage($event)"></el-input>
|
||||
<el-input v-model="data.message.query.model" placeholder="模型" class="handle-input mr10" @keyup="searchMessage($event)"></el-input>
|
||||
<el-input
|
||||
v-model.number="data.message.query.user_id"
|
||||
placeholder="账户ID"
|
||||
class="handle-input mr10"
|
||||
@keyup="searchMessage($event)"
|
||||
></el-input>
|
||||
<el-input
|
||||
v-model="data.message.query.content"
|
||||
placeholder="消息内容"
|
||||
class="handle-input mr10"
|
||||
@keyup="searchMessage($event)"
|
||||
></el-input>
|
||||
<el-input
|
||||
v-model="data.message.query.model"
|
||||
placeholder="模型"
|
||||
class="handle-input mr10"
|
||||
@keyup="searchMessage($event)"
|
||||
></el-input>
|
||||
<el-date-picker
|
||||
v-model="data.message.query.created_at"
|
||||
type="daterange"
|
||||
@@ -108,13 +135,15 @@
|
||||
|
||||
<el-table-column label="创建时间">
|
||||
<template #default="scope">
|
||||
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
|
||||
<span>{{ dateFormat(scope.row['created_at']) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="showContent(scope.row.content)">查看</el-button>
|
||||
<el-button size="small" type="primary" @click="showContent(scope.row.content)"
|
||||
>查看</el-button
|
||||
>
|
||||
<el-popconfirm title="确定要删除当前记录吗?" @confirm="removeMessage(scope.row)">
|
||||
<template #reference>
|
||||
<el-button size="small" type="danger">删除</el-button>
|
||||
@@ -140,13 +169,23 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="showContentDialog" title="消息详情" class="chat-dialog" style="--el-dialog-width: 60%">
|
||||
<el-dialog
|
||||
v-model="showContentDialog"
|
||||
title="消息详情"
|
||||
class="chat-dialog"
|
||||
style="--el-dialog-width: 60%"
|
||||
>
|
||||
<div class="chat-detail">
|
||||
<div class="chat-line" v-html="dialogContent"></div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showChatItemDialog" title="对话详情" class="chat-dialog" style="--el-dialog-width: 60%">
|
||||
<el-dialog
|
||||
v-model="showChatItemDialog"
|
||||
title="对话详情"
|
||||
class="chat-dialog"
|
||||
style="--el-dialog-width: 60%"
|
||||
>
|
||||
<div class="chat-box chat-page">
|
||||
<div v-for="item in messages" :key="item.id">
|
||||
<chat-prompt v-if="item.type === 'prompt'" :data="item" />
|
||||
@@ -159,21 +198,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { dateFormat, processContent } from "@/utils/libs";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import "highlight.js/styles/a11y-dark.css";
|
||||
import hl from "highlight.js";
|
||||
import ChatPrompt from "@/components/ChatPrompt.vue";
|
||||
import ChatReply from "@/components/ChatReply.vue";
|
||||
import ChatPrompt from '@/components/ChatPrompt.vue'
|
||||
import ChatReply from '@/components/ChatReply.vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { dateFormat, processContent } from '@/utils/libs'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import hl from 'highlight.js'
|
||||
import 'highlight.js/styles/a11y-dark.css'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
// 变量定义
|
||||
const data = ref({
|
||||
chat: {
|
||||
items: [],
|
||||
query: { title: "", created_at: [], page: 1, page_size: 15 },
|
||||
query: { title: '', created_at: [], page: 1, page_size: 15 },
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
@@ -181,104 +220,104 @@ const data = ref({
|
||||
},
|
||||
message: {
|
||||
items: [],
|
||||
query: { title: "", created_at: [], page: 1, page_size: 15 },
|
||||
query: { title: '', created_at: [], page: 1, page_size: 15 },
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: 15,
|
||||
loading: true,
|
||||
},
|
||||
});
|
||||
const activeName = ref("chat");
|
||||
})
|
||||
const activeName = ref('chat')
|
||||
|
||||
onMounted(() => {
|
||||
fetchChatData();
|
||||
});
|
||||
fetchChatData()
|
||||
})
|
||||
|
||||
const handleChange = (tab) => {
|
||||
if (tab === "chat") {
|
||||
fetchChatData();
|
||||
} else if (tab === "message") {
|
||||
fetchMessageData();
|
||||
if (tab === 'chat') {
|
||||
fetchChatData()
|
||||
} else if (tab === 'message') {
|
||||
fetchMessageData()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 搜索对话
|
||||
const searchChat = (evt) => {
|
||||
if (evt.keyCode === 13) {
|
||||
fetchChatData();
|
||||
fetchChatData()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 搜索消息
|
||||
const searchMessage = (evt) => {
|
||||
if (evt.keyCode === 13) {
|
||||
fetchMessageData();
|
||||
fetchMessageData()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchChatData = () => {
|
||||
const d = data.value.chat;
|
||||
d.query.page = d.page;
|
||||
d.query.page_size = d.pageSize;
|
||||
httpPost("/api/admin/chat/list", d.query)
|
||||
const d = data.value.chat
|
||||
d.query.page = d.page
|
||||
d.query.page_size = d.pageSize
|
||||
httpPost('/api/admin/chat/list', d.query)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
d.items = res.data.items;
|
||||
d.total = res.data.total;
|
||||
d.page = res.data.page;
|
||||
d.pageSize = res.data.page_size;
|
||||
d.items = res.data.items
|
||||
d.total = res.data.total
|
||||
d.page = res.data.page
|
||||
d.pageSize = res.data.page_size
|
||||
}
|
||||
d.loading = false;
|
||||
d.loading = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const fetchMessageData = () => {
|
||||
const d = data.value.message;
|
||||
d.query.page = d.page;
|
||||
d.query.page_size = d.pageSize;
|
||||
httpPost("/api/admin/chat/message", d.query)
|
||||
const d = data.value.message
|
||||
d.query.page = d.page
|
||||
d.query.page_size = d.pageSize
|
||||
httpPost('/api/admin/chat/message', d.query)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
d.items = res.data.items;
|
||||
d.total = res.data.total;
|
||||
d.page = res.data.page;
|
||||
d.pageSize = res.data.page_size;
|
||||
d.items = res.data.items
|
||||
d.total = res.data.total
|
||||
d.page = res.data.page
|
||||
d.pageSize = res.data.page_size
|
||||
}
|
||||
d.loading = false;
|
||||
d.loading = false
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取数据失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('获取数据失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeChat = function (row) {
|
||||
httpGet("/api/admin/chat/remove?chat_id=" + row.chat_id)
|
||||
httpGet('/api/admin/chat/remove?chat_id=' + row.chat_id)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功!");
|
||||
fetchChatData();
|
||||
ElMessage.success('删除成功!')
|
||||
fetchChatData()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('删除失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeMessage = function (row) {
|
||||
httpGet("/api/admin/chat/message/remove?id=" + row.id)
|
||||
httpGet('/api/admin/chat/message/remove?id=' + row.id)
|
||||
.then(() => {
|
||||
ElMessage.success("删除成功!");
|
||||
fetchMessageData();
|
||||
ElMessage.success('删除成功!')
|
||||
fetchMessageData()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("删除失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('删除失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -286,43 +325,43 @@ const md = require("markdown-it")({
|
||||
highlight: function (str, lang) {
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
// 处理代码高亮
|
||||
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></pre>`;
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></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></pre>`;
|
||||
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code></pre>`
|
||||
},
|
||||
});
|
||||
md.use(mathjaxPlugin);
|
||||
})
|
||||
md.use(mathjaxPlugin)
|
||||
|
||||
const showContentDialog = ref(false);
|
||||
const dialogContent = ref("");
|
||||
const showContentDialog = ref(false)
|
||||
const dialogContent = ref('')
|
||||
const showContent = (content) => {
|
||||
showContentDialog.value = true;
|
||||
dialogContent.value = md.render(processContent(content));
|
||||
};
|
||||
showContentDialog.value = true
|
||||
dialogContent.value = md.render(processContent(content))
|
||||
}
|
||||
|
||||
const showChatItemDialog = ref(false);
|
||||
const messages = ref([]);
|
||||
const showChatItemDialog = ref(false)
|
||||
const messages = ref([])
|
||||
const showMessages = (row) => {
|
||||
showChatItemDialog.value = true;
|
||||
messages.value = [];
|
||||
httpGet("/api/admin/chat/history?chat_id=" + row.chat_id)
|
||||
showChatItemDialog.value = true
|
||||
messages.value = []
|
||||
httpGet('/api/admin/chat/history?chat_id=' + row.chat_id)
|
||||
.then((res) => {
|
||||
const data = res.data;
|
||||
const data = res.data
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
messages.value.push(data[i]);
|
||||
messages.value.push(data[i])
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
// TODO: 显示重新加载按钮
|
||||
ElMessage.error("加载聊天记录失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error('加载聊天记录失败:' + e.message)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
@@ -380,7 +419,7 @@ const showMessages = (row) => {
|
||||
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
}
|
||||
}
|
||||
268
web/src/views/mobile/Apps.vue
Normal file
268
web/src/views/mobile/Apps.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div class="apps-page">
|
||||
<van-nav-bar title="全部应用" left-arrow @click-left="router.back()" />
|
||||
|
||||
<div class="apps-filter mb-8 pt-8" style="border: 1px solid #ccc">
|
||||
<van-tabs v-model="activeTab" animated swipeable>
|
||||
<van-tab title="全部分类">
|
||||
<div class="app-list">
|
||||
<van-list v-model="loading" :finished="true" finished-text="" @load="fetchApps()">
|
||||
<van-cell v-for="item in apps" :key="item.id" class="app-cell">
|
||||
<div class="app-card">
|
||||
<div class="app-info">
|
||||
<div class="app-image">
|
||||
<van-image :src="item.icon" round />
|
||||
</div>
|
||||
<div class="app-detail">
|
||||
<div class="app-title">{{ item.name }}</div>
|
||||
<div class="app-desc">{{ item.hello_msg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-actions">
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
class="action-btn"
|
||||
@click="useRole(item.id)"
|
||||
>对话</van-button
|
||||
>
|
||||
<van-button
|
||||
size="small"
|
||||
:type="hasRole(item.key) ? 'danger' : 'success'"
|
||||
class="action-btn"
|
||||
@click="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
|
||||
>
|
||||
{{ hasRole(item.key) ? '移除' : '添加' }}
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-cell>
|
||||
</van-list>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab v-for="type in appTypes" :key="type.id" :title="type.name">
|
||||
<div class="app-list">
|
||||
<van-list
|
||||
v-model="loading"
|
||||
:finished="true"
|
||||
finished-text=""
|
||||
@load="fetchApps(type.id)"
|
||||
>
|
||||
<van-cell v-for="item in typeApps" :key="item.id" class="app-cell">
|
||||
<div class="app-card">
|
||||
<div class="app-info">
|
||||
<div class="app-image">
|
||||
<van-image :src="item.icon" round />
|
||||
</div>
|
||||
<div class="app-detail">
|
||||
<div class="app-title">{{ item.name }}</div>
|
||||
<div class="app-desc">{{ item.hello_msg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-actions">
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
class="action-btn"
|
||||
@click="useRole(item.id)"
|
||||
>对话</van-button
|
||||
>
|
||||
<van-button
|
||||
size="small"
|
||||
:type="hasRole(item.key) ? 'danger' : 'success'"
|
||||
class="action-btn"
|
||||
@click="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
|
||||
>
|
||||
{{ hasRole(item.key) ? '移除' : '添加' }}
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-cell>
|
||||
</van-list>
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { arrayContains, removeArrayItem, showLoginDialog, substr } from '@/utils/libs'
|
||||
import { showNotify } from 'vant'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const isLogin = ref(false)
|
||||
const apps = ref([])
|
||||
const typeApps = ref([])
|
||||
const appTypes = ref([])
|
||||
const loading = ref(false)
|
||||
const roles = ref([])
|
||||
const activeTab = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
isLogin.value = true
|
||||
roles.value = user.chat_roles
|
||||
})
|
||||
.catch(() => {})
|
||||
fetchAppTypes()
|
||||
fetchApps()
|
||||
})
|
||||
|
||||
const fetchAppTypes = () => {
|
||||
httpGet('/api/app/type/list')
|
||||
.then((res) => {
|
||||
appTypes.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取应用分类失败:' + e.message })
|
||||
})
|
||||
}
|
||||
|
||||
const fetchApps = (typeId = '') => {
|
||||
httpGet('/api/app/list', { tid: typeId })
|
||||
.then((res) => {
|
||||
const items = res.data
|
||||
// 处理 hello message
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
items[i].intro = substr(items[i].hello_msg, 80)
|
||||
}
|
||||
|
||||
if (typeId) {
|
||||
typeApps.value = items
|
||||
} else {
|
||||
apps.value = items
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取应用失败:' + e.message })
|
||||
})
|
||||
}
|
||||
|
||||
const updateRole = (row, opt) => {
|
||||
if (!isLogin.value) {
|
||||
return showLoginDialog(router)
|
||||
}
|
||||
|
||||
const title = ref('')
|
||||
if (opt === 'add') {
|
||||
title.value = '添加应用'
|
||||
const exists = arrayContains(roles.value, row.key)
|
||||
if (exists) {
|
||||
return
|
||||
}
|
||||
roles.value.push(row.key)
|
||||
} else {
|
||||
title.value = '移除应用'
|
||||
const exists = arrayContains(roles.value, row.key)
|
||||
if (!exists) {
|
||||
return
|
||||
}
|
||||
roles.value = removeArrayItem(roles.value, row.key)
|
||||
}
|
||||
httpPost('/api/app/update', { keys: roles.value })
|
||||
.then(() => {
|
||||
showNotify({ type: 'success', message: title.value + '成功!' })
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: title.value + '失败:' + e.message })
|
||||
})
|
||||
}
|
||||
|
||||
const hasRole = (roleKey) => {
|
||||
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
|
||||
}
|
||||
|
||||
const useRole = (roleId) => {
|
||||
if (!isLogin.value) {
|
||||
return showLoginDialog(router)
|
||||
}
|
||||
router.push(`/mobile/chat/session?role_id=${roleId}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.apps-page {
|
||||
min-height 100vh
|
||||
background-color var(--van-background)
|
||||
|
||||
.apps-filter {
|
||||
padding 10px 0
|
||||
|
||||
:deep(.van-tabs__nav) {
|
||||
background var(--van-background-2)
|
||||
}
|
||||
}
|
||||
|
||||
.app-list {
|
||||
padding 0 15px
|
||||
|
||||
.app-cell {
|
||||
padding 0
|
||||
margin-bottom 15px
|
||||
|
||||
.app-card {
|
||||
background var(--van-cell-background)
|
||||
border-radius 12px
|
||||
padding 15px
|
||||
box-shadow 0 2px 12px rgba(0, 0, 0, 0.05)
|
||||
|
||||
.app-info {
|
||||
display flex
|
||||
align-items center
|
||||
margin-bottom 15px
|
||||
|
||||
.app-image {
|
||||
width 60px
|
||||
height 60px
|
||||
margin-right 15px
|
||||
|
||||
:deep(.van-image) {
|
||||
width 100%
|
||||
height 100%
|
||||
}
|
||||
}
|
||||
|
||||
.app-detail {
|
||||
flex 1
|
||||
|
||||
.app-title {
|
||||
font-size 16px
|
||||
font-weight 600
|
||||
margin-bottom 5px
|
||||
color var(--van-text-color)
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
font-size 13px
|
||||
color var(--van-gray-6)
|
||||
display -webkit-box
|
||||
-webkit-box-orient vertical
|
||||
-webkit-line-clamp 2
|
||||
overflow hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-actions {
|
||||
display flex
|
||||
gap 10px
|
||||
|
||||
.action-btn {
|
||||
flex 1
|
||||
border-radius 20px
|
||||
padding 0 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +1,79 @@
|
||||
<template>
|
||||
<div class="index container">
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
<van-notice-bar left-icon="info-o" :scrollable="true">{{ slogan }}}</van-notice-bar>
|
||||
<div class="header">
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<van-grid :column-num="3" :gutter="10" border>
|
||||
<van-grid-item @click="router.push('chat')">
|
||||
<template #icon>
|
||||
<i class="iconfont icon-chat"></i>
|
||||
</template>
|
||||
<template #text>
|
||||
<div class="text">AI 对话</div>
|
||||
</template>
|
||||
</van-grid-item>
|
||||
<div class="content mb-8">
|
||||
<div class="feature-grid">
|
||||
<van-grid :column-num="3" :gutter="15" border>
|
||||
<van-grid-item @click="router.push('chat')" class="feature-item">
|
||||
<template #icon>
|
||||
<div class="feature-icon">
|
||||
<i class="iconfont icon-chat"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template #text>
|
||||
<div class="text">AI 对话</div>
|
||||
</template>
|
||||
</van-grid-item>
|
||||
|
||||
<van-grid-item @click="router.push('image')">
|
||||
<template #icon>
|
||||
<i class="iconfont icon-mj"></i>
|
||||
</template>
|
||||
<template #text>
|
||||
<div class="text">AI 绘画</div>
|
||||
</template>
|
||||
</van-grid-item>
|
||||
<van-grid-item @click="router.push('image')" class="feature-item">
|
||||
<template #icon>
|
||||
<div class="feature-icon">
|
||||
<i class="iconfont icon-mj"></i>
|
||||
</div>
|
||||
</template>
|
||||
<template #text>
|
||||
<div class="text">AI 绘画</div>
|
||||
</template>
|
||||
</van-grid-item>
|
||||
|
||||
<van-grid-item @click="router.push('imgWall')">
|
||||
<template #icon>
|
||||
<van-icon name="photo-o" />
|
||||
</template>
|
||||
<template #text>
|
||||
<div class="text">AI 画廊</div>
|
||||
</template>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
<van-grid-item @click="router.push('imgWall')" class="feature-item">
|
||||
<template #icon>
|
||||
<div class="feature-icon">
|
||||
<van-icon name="photo-o" />
|
||||
</div>
|
||||
</template>
|
||||
<template #text>
|
||||
<div class="text">AI 画廊</div>
|
||||
</template>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">推荐应用</h3>
|
||||
<van-button class="more-btn" size="small" icon="arrow" @click="router.push('apps')"
|
||||
>更多</van-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="app-list">
|
||||
<van-list v-model:loading="loading" :finished="true" finished-text="" @load="fetchApps">
|
||||
<van-cell v-for="item in apps" :key="item.id">
|
||||
<div>
|
||||
<div class="item">
|
||||
<div class="image flex justify-center items-center">
|
||||
<van-image :src="item.icon" />
|
||||
<van-list v-model="loading" :finished="true" finished-text="" @load="fetchApps">
|
||||
<van-cell v-for="item in displayApps" :key="item.id" class="app-cell">
|
||||
<div class="app-card">
|
||||
<div class="app-info">
|
||||
<div class="app-image">
|
||||
<van-image :src="item.icon" round />
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="info-title">{{ item.name }}</div>
|
||||
<div class="info-text">{{ item.hello_msg }}</div>
|
||||
<div class="app-detail">
|
||||
<div class="app-title">{{ item.name }}</div>
|
||||
<div class="app-desc">{{ item.hello_msg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn">
|
||||
<div v-if="hasRole(item.key)">
|
||||
<van-button size="small" type="success" @click="useRole(item.id)">使用</van-button>
|
||||
<van-button size="small" type="danger" @click="updateRole(item, 'remove')">移除</van-button>
|
||||
</div>
|
||||
<van-button v-else size="small" style="--el-color-primary: #009999" @click="updateRole(item, 'add')">
|
||||
<van-icon name="add-o" />
|
||||
<span>添加应用</span>
|
||||
<div class="app-actions">
|
||||
<van-button size="small" type="primary" class="action-btn" @click="useRole(item.id)"
|
||||
>对话</van-button
|
||||
>
|
||||
<van-button
|
||||
size="small"
|
||||
:type="hasRole(item.key) ? 'danger' : 'success'"
|
||||
class="action-btn"
|
||||
@click="updateRole(item, hasRole(item.key) ? 'remove' : 'add')"
|
||||
>
|
||||
{{ hasRole(item.key) ? '移除' : '添加' }}
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,173 +85,245 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { checkSession, getSystemInfo } from "@/store/cache";
|
||||
import { httpGet, httpPost } from "@/utils/http";
|
||||
import { arrayContains, removeArrayItem, showLoginDialog, substr } from "@/utils/libs";
|
||||
import { showNotify } from "vant";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { arrayContains, removeArrayItem, showLoginDialog, substr } from '@/utils/libs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { showNotify } from 'vant'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const title = ref(process.env.VUE_APP_TITLE);
|
||||
const router = useRouter();
|
||||
const isLogin = ref(false);
|
||||
const apps = ref([]);
|
||||
const loading = ref(false);
|
||||
const roles = ref([]);
|
||||
const slogan = ref("你有多大想象力,AI就有多大创造力!");
|
||||
const title = ref(process.env.VUE_APP_TITLE)
|
||||
const router = useRouter()
|
||||
const isLogin = ref(false)
|
||||
const apps = ref([])
|
||||
const loading = ref(false)
|
||||
const roles = ref([])
|
||||
const slogan = ref('你有多大想象力,AI就有多大创造力!')
|
||||
|
||||
// 只显示前5个应用
|
||||
const displayApps = computed(() => {
|
||||
return apps.value.slice(0, 8)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
title.value = res.data.title;
|
||||
title.value = res.data.title
|
||||
if (res.data.slogan) {
|
||||
slogan.value = res.data.slogan;
|
||||
slogan.value = res.data.slogan
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error("获取系统配置失败:" + e.message);
|
||||
});
|
||||
ElMessage.error('获取系统配置失败:' + e.message)
|
||||
})
|
||||
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
isLogin.value = true;
|
||||
roles.value = user.chat_roles;
|
||||
isLogin.value = true
|
||||
roles.value = user.chat_roles
|
||||
})
|
||||
.catch(() => {});
|
||||
fetchApps();
|
||||
});
|
||||
.catch(() => {})
|
||||
fetchApps()
|
||||
})
|
||||
|
||||
const fetchApps = () => {
|
||||
httpGet("/api/app/list")
|
||||
httpGet('/api/app/list')
|
||||
.then((res) => {
|
||||
const items = res.data;
|
||||
const items = res.data
|
||||
// 处理 hello message
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
items[i].intro = substr(items[i].hello_msg, 80);
|
||||
items[i].intro = substr(items[i].hello_msg, 80)
|
||||
}
|
||||
apps.value = items;
|
||||
apps.value = items
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: "danger", message: "获取应用失败:" + e.message });
|
||||
});
|
||||
};
|
||||
showNotify({ type: 'danger', message: '获取应用失败:' + e.message })
|
||||
})
|
||||
}
|
||||
|
||||
const updateRole = (row, opt) => {
|
||||
if (!isLogin.value) {
|
||||
return showLoginDialog(router);
|
||||
return showLoginDialog(router)
|
||||
}
|
||||
|
||||
const title = ref("");
|
||||
if (opt === "add") {
|
||||
title.value = "添加应用";
|
||||
const exists = arrayContains(roles.value, row.key);
|
||||
const title = ref('')
|
||||
if (opt === 'add') {
|
||||
title.value = '添加应用'
|
||||
const exists = arrayContains(roles.value, row.key)
|
||||
if (exists) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
roles.value.push(row.key);
|
||||
roles.value.push(row.key)
|
||||
} else {
|
||||
title.value = "移除应用";
|
||||
const exists = arrayContains(roles.value, row.key);
|
||||
title.value = '移除应用'
|
||||
const exists = arrayContains(roles.value, row.key)
|
||||
if (!exists) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
roles.value = removeArrayItem(roles.value, row.key);
|
||||
roles.value = removeArrayItem(roles.value, row.key)
|
||||
}
|
||||
httpPost("/api/role/update", { keys: roles.value })
|
||||
httpPost('/api/app/update', { keys: roles.value })
|
||||
.then(() => {
|
||||
ElMessage.success({ message: title.value + "成功!", duration: 1000 });
|
||||
ElMessage.success({ message: title.value + '成功!', duration: 1000 })
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error(title.value + "失败:" + e.message);
|
||||
});
|
||||
};
|
||||
ElMessage.error(title.value + '失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const hasRole = (roleKey) => {
|
||||
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2);
|
||||
};
|
||||
return arrayContains(roles.value, roleKey, (v1, v2) => v1 === v2)
|
||||
}
|
||||
|
||||
const useRole = (roleId) => {
|
||||
if (!isLogin.value) {
|
||||
return showLoginDialog(router);
|
||||
return showLoginDialog(router)
|
||||
}
|
||||
router.push(`/mobile/chat/session?role_id=${roleId}`);
|
||||
};
|
||||
router.push(`/mobile/chat/session?role_id=${roleId}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="stylus">
|
||||
.index {
|
||||
color var(--van-text-color)
|
||||
.title {
|
||||
display flex
|
||||
justify-content center
|
||||
background-color var(--van-background)
|
||||
min-height 100vh
|
||||
display flex
|
||||
flex-direction column
|
||||
|
||||
.header {
|
||||
flex-shrink 0
|
||||
padding 10px 15px
|
||||
text-align center
|
||||
background var(--van-background)
|
||||
position sticky
|
||||
top 0
|
||||
z-index 1
|
||||
|
||||
.title {
|
||||
font-size 24px
|
||||
font-weight 600
|
||||
color var(--van-text-color)
|
||||
}
|
||||
|
||||
.slogan {
|
||||
font-size 14px
|
||||
color var(--van-gray-6)
|
||||
}
|
||||
}
|
||||
--van-notice-bar-font-size: 16px
|
||||
|
||||
.content {
|
||||
padding 15px 0 60px 0
|
||||
.van-grid-item {
|
||||
.iconfont {
|
||||
font-size 20px
|
||||
flex 1
|
||||
overflow-y auto
|
||||
padding 15px
|
||||
-webkit-overflow-scrolling touch
|
||||
|
||||
.feature-grid {
|
||||
margin-bottom 30px
|
||||
|
||||
.feature-item {
|
||||
padding 15px 0
|
||||
|
||||
.feature-icon {
|
||||
width 50px
|
||||
height 50px
|
||||
border-radius 50%
|
||||
background var(--van-primary-color)
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
margin-bottom 10px
|
||||
|
||||
i, .van-icon {
|
||||
font-size 24px
|
||||
color white
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size 14px
|
||||
font-weight 500
|
||||
}
|
||||
}
|
||||
.text {
|
||||
display flex
|
||||
width 100%
|
||||
padding 10px
|
||||
justify-content center
|
||||
font-size 14px
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display flex
|
||||
justify-content space-between
|
||||
align-items center
|
||||
margin-bottom 15px
|
||||
|
||||
.section-title {
|
||||
font-size 18px
|
||||
font-weight 600
|
||||
color var(--van-text-color)
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
padding 0 10px
|
||||
font-size 12px
|
||||
border-radius 15px
|
||||
}
|
||||
}
|
||||
|
||||
.app-list {
|
||||
padding-top 10px
|
||||
.app-cell {
|
||||
padding 0
|
||||
margin-bottom 15px
|
||||
|
||||
.item {
|
||||
display flex
|
||||
.image {
|
||||
width 80px
|
||||
height 80px
|
||||
min-width 80px
|
||||
border-radius 5px
|
||||
overflow hidden
|
||||
}
|
||||
.app-card {
|
||||
background var(--van-cell-background)
|
||||
border-radius 12px
|
||||
padding 15px
|
||||
box-shadow 0 2px 12px rgba(0, 0, 0, 0.05)
|
||||
|
||||
.info {
|
||||
text-align left
|
||||
padding 0 10px
|
||||
.info-title {
|
||||
color var(--van-text-color)
|
||||
font-size 1.25rem
|
||||
line-height 1.75rem
|
||||
letter-spacing: .025em;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
.app-info {
|
||||
display flex
|
||||
align-items center
|
||||
margin-bottom 15px
|
||||
|
||||
.app-image {
|
||||
width 60px
|
||||
height 60px
|
||||
margin-right 15px
|
||||
|
||||
:deep(.van-image) {
|
||||
width 100%
|
||||
height 100%
|
||||
}
|
||||
}
|
||||
|
||||
.app-detail {
|
||||
flex 1
|
||||
|
||||
.app-title {
|
||||
font-size 16px
|
||||
font-weight 600
|
||||
margin-bottom 5px
|
||||
color var(--van-text-color)
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
font-size 13px
|
||||
color var(--van-gray-6)
|
||||
display -webkit-box
|
||||
-webkit-box-orient vertical
|
||||
-webkit-line-clamp 2
|
||||
overflow hidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-text {
|
||||
padding 5px 0
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
word-break: break-all;
|
||||
font-size: .875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.app-actions {
|
||||
display flex
|
||||
gap 10px
|
||||
|
||||
.btn {
|
||||
padding 5px 0
|
||||
.van-button {
|
||||
margin-right 10px
|
||||
|
||||
.van-icon {
|
||||
margin-right 5px
|
||||
.action-btn {
|
||||
flex 1
|
||||
border-radius 20px
|
||||
padding 0 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,9 @@ module.exports = defineConfig({
|
||||
outputDir: "dist",
|
||||
crossorigin: "anonymous",
|
||||
devServer: {
|
||||
client: {
|
||||
overlay: false // 关闭错误覆盖层
|
||||
},
|
||||
allowedHosts: "all",
|
||||
port: 8888,
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user