mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-09-17 10:47:15 +00:00
release: v4.2.8
整合开源版 v4.2.8 功能:Sora2 视频、路由重构、手机站开关、DALL-E 参考图,以及启动时自动同步数据表字段等修复与优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -83,7 +83,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
.el-input__wrapper {
|
||||
background: var(--card-bg);
|
||||
//background: var(--card-bg);
|
||||
}
|
||||
.el-dialog__title {
|
||||
font-weight: bold;
|
||||
@@ -138,7 +138,8 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tag, .el-tag.el-tag--primary {
|
||||
.el-tag,
|
||||
.el-tag.el-tag--primary {
|
||||
--el-tag-bg-color: #f0ebff;
|
||||
}
|
||||
.box-card {
|
||||
@@ -148,4 +149,4 @@
|
||||
}
|
||||
.el-table th.el-table__cell {
|
||||
background-color: var(--chat-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,10 +189,11 @@
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #42b983;
|
||||
border-left: 5px solid #c1c1c1;
|
||||
padding: 10px 15px;
|
||||
color: #777;
|
||||
background-color: rgba(66, 185, 131, 0.1);
|
||||
background-color: rgba(193, 193, 193, 0.2);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div v-if="list.length" class="space-y-3 mb-2">
|
||||
<div
|
||||
v-for="(f, idx) in list"
|
||||
:key="f.url || idx"
|
||||
class="flex !items-start flex-col justify-center gap-3 p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||
>
|
||||
<!-- Image -->
|
||||
<div
|
||||
v-if="isImageFile(f)"
|
||||
class="flex-shrink-0 bg-gray-100 max-w-[500px] max-h-[500px] dark:bg-gray-700 rounded-lg overflow-hidden"
|
||||
>
|
||||
<el-image
|
||||
:src="f.url"
|
||||
fit="cover"
|
||||
:preview-src-list="imageUrls"
|
||||
:initial-index="imageIndexMap.get(f.url) || 0"
|
||||
hide-on-click-modal
|
||||
:z-index="3000"
|
||||
class="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Video -->
|
||||
<div
|
||||
v-else-if="isVideoFile(f)"
|
||||
class="flex-shrink-0 bg-gray-100 max-w-[500px] dark:bg-gray-700 rounded-lg overflow-hidden"
|
||||
>
|
||||
<video :src="f.url" controls preload="metadata" class="w-full h-full object-cover"></video>
|
||||
</div>
|
||||
|
||||
<!-- Audio -->
|
||||
<div
|
||||
v-else-if="isAudioFile(f)"
|
||||
class="flex-shrink-0 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<audio :src="f.url" controls preload="metadata"></audio>
|
||||
</div>
|
||||
|
||||
<!-- Other Files -->
|
||||
<div
|
||||
v-else
|
||||
class="flex-shrink-0 w-20 h-20 bg-gray-100 dark:bg-gray-700 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<img :src="GetFileIcon(extOf(f))" class="w-20 h-20" alt="file" />
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-row min-w-0 justify-between items-center">
|
||||
<!-- File Info -->
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ extOf(f).replace('.', '').toUpperCase() || 'FILE' }} ·
|
||||
{{ FormatFileSize(f.size || 0) }}
|
||||
</div>
|
||||
|
||||
<!-- Download Button -->
|
||||
<div class="flex-shrink-0">
|
||||
<el-tooltip class="box-item" effect="dark" content="下载" placement="top">
|
||||
<i
|
||||
class="iconfont icon-download !text-sm cursor-pointer"
|
||||
v-if="!f.downloading"
|
||||
@click="downloadFile(f)"
|
||||
></i>
|
||||
<el-image src="/images/loading.gif" class="w-4 h-4" fit="cover" v-else />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { FormatFileSize, GetFileIcon } from '@/store/system'
|
||||
import { computed } from 'vue'
|
||||
import { httpDownload } from '@/utils/http'
|
||||
import { replaceImg } from '@/utils/libs'
|
||||
|
||||
const props = defineProps({
|
||||
files: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const list = computed(() => props.files || [])
|
||||
|
||||
const normalizeExt = (ext) => (ext || '').toLowerCase()
|
||||
const urlExt = (url) => {
|
||||
if (!url) return ''
|
||||
try {
|
||||
const path = url.split('?')[0]
|
||||
const dot = path.lastIndexOf('.')
|
||||
return dot >= 0 ? path.substring(dot) : ''
|
||||
} catch (_) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const extOf = (f) => normalizeExt(f.ext || urlExt(f.url))
|
||||
|
||||
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'])
|
||||
const VIDEO_EXTS = new Set(['.mp4', '.webm', '.ogg', '.mov', '.m4v'])
|
||||
const AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg', '.aac', '.m4a'])
|
||||
|
||||
const isImageFile = (f) => IMAGE_EXTS.has(extOf(f))
|
||||
const isVideoFile = (f) => VIDEO_EXTS.has(extOf(f))
|
||||
const isAudioFile = (f) => AUDIO_EXTS.has(extOf(f))
|
||||
|
||||
// image preview urls and index mapping for el-image gallery
|
||||
const imageUrls = computed(() => list.value.filter((f) => isImageFile(f)).map((f) => f.url))
|
||||
const imageIndexMap = computed(() => {
|
||||
const map = new Map()
|
||||
imageUrls.value.forEach((u, i) => map.set(u, i))
|
||||
return map
|
||||
})
|
||||
|
||||
const downloadFile = async (item) => {
|
||||
const url = replaceImg(item.url)
|
||||
const downloadURL = `/api/download?url=${url}`
|
||||
const urlObj = new URL(url)
|
||||
const fileName = urlObj.pathname.split('/').pop()
|
||||
|
||||
item.downloading = true
|
||||
|
||||
try {
|
||||
const response = await httpDownload(downloadURL)
|
||||
const blob = new Blob([response.data])
|
||||
const link = document.createElement('a')
|
||||
link.href = URL.createObjectURL(blob)
|
||||
link.download = fileName
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(link.href)
|
||||
item.downloading = false
|
||||
} catch (error) {
|
||||
showMessageError('下载失败')
|
||||
item.downloading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -9,7 +9,7 @@
|
||||
<div v-if="files && files.length > 0" class="file-list-box">
|
||||
<div v-for="file in files" :key="file.url">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
<el-image :src="file.url" fit="cover" :preview-src-list="[file.url]" />
|
||||
</div>
|
||||
<div class="item" v-else>
|
||||
<div class="icon">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<img :src="data.icon" alt="ChatGPT" />
|
||||
</div>
|
||||
<div class="chat-item">
|
||||
<div class="content-wrapper">
|
||||
<div class="content-wrapper flex-col">
|
||||
<div
|
||||
class="content"
|
||||
v-html="md.render(processContent(data.content.text))"
|
||||
@@ -15,6 +15,12 @@
|
||||
<div class="content flex justify-start items-center" v-else>
|
||||
<span class="mr-2">AI 思考中</span> <Thinking :duration="1.5" />
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<AttachmentList
|
||||
v-if="data.content && data.content.files && data.content.files.length"
|
||||
:files="data.content.files"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex text-gray-500 text-sm py-2 items-center space-x-2"
|
||||
@@ -79,6 +85,7 @@ import emoji from 'markdown-it-emoji'
|
||||
import mathjaxPlugin from 'markdown-it-mathjax3'
|
||||
import { nextTick, onMounted, reactive, ref, watchEffect } from 'vue'
|
||||
import Thinking from './Thinking.vue'
|
||||
import AttachmentList from './AttachmentList.vue'
|
||||
// eslint-disable-next-line no-undef,no-unused-vars
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@@ -118,7 +125,7 @@ const md = new MarkdownIt({
|
||||
// 显示复制代码按钮和展开/收起按钮
|
||||
const copyBtn = `<div class="flex">
|
||||
<span class="text-[12px] mr-2 text-[#00e0e0] cursor-pointer expand-btn" data-code-id="${codeIndex}" onclick="window.toggleCodeBlock('${codeIndex}')">收起</span>
|
||||
<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
|
||||
<span class="copy-code-btn text-blue-500 text-sm cursor-pointer" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
|
||||
</div><textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
|
||||
/<\/textarea>/g,
|
||||
'</textarea>'
|
||||
@@ -127,7 +134,7 @@ const md = new MarkdownIt({
|
||||
let preCode = ''
|
||||
// 处理代码高亮
|
||||
if (lang && hl.getLanguage(lang)) {
|
||||
langHtml = `<span class="lang-name">${lang}</span>`
|
||||
langHtml = `<span class="lang-name text-white">${lang}</span>`
|
||||
preCode = hl.highlight(str, { language: lang }).value
|
||||
} else {
|
||||
preCode = md.utils.escapeHtml(str)
|
||||
@@ -137,7 +144,7 @@ const md = new MarkdownIt({
|
||||
return `<pre class="code-container flex flex-col code-expanded" data-code-id="${codeIndex}">
|
||||
<div class="flex justify-between bg-[#50505a] w-full rounded-tl-[10px] rounded-tr-[10px] px-3 py-1">${langHtml}${copyBtn}</div>
|
||||
<code class="language-${lang} hljs">${preCode}</code>
|
||||
<span class="copy-code-btn absolute right-3 bottom-3" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span></pre>`
|
||||
<span class="copy-code-btn absolute right-3 bottom-3 text-white text-sm cursor-pointer" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span></pre>`
|
||||
},
|
||||
})
|
||||
md.use(mathjaxPlugin)
|
||||
@@ -313,6 +320,16 @@ const setupCodeBlockEvents = () => {
|
||||
border-radius: 0 10px 10px 10px;
|
||||
width: 100%;
|
||||
|
||||
.code-expanded {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre code.hljs {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -320,6 +337,10 @@ const setupCodeBlockEvents = () => {
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
think {
|
||||
border: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-container class="chat-file-list">
|
||||
<div v-for="file in fileList" :key="file.url">
|
||||
<div class="image" v-if="isImage(file.ext)">
|
||||
<el-image :src="file.url" fit="cover" />
|
||||
<el-image :src="file.url" fit="cover" :preview-src-list="[file.url]" />
|
||||
<div class="action">
|
||||
<el-icon @click="removeFile(file)"><CircleCloseFilled /></el-icon>
|
||||
</div>
|
||||
|
||||
@@ -11,22 +11,26 @@
|
||||
<span class="mr-2">{{ copyRight }}</span>
|
||||
</div>
|
||||
<div class="flex justify-center text-sm">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank">ICP备案:{{ icp }}</a>
|
||||
<span>|</span>
|
||||
<img :src="gaBeianImg" class="w-4 h-4 mx-1" alt="beian" />
|
||||
<a
|
||||
:href="`http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=${getCodeNum(
|
||||
gaBeian
|
||||
)}`"
|
||||
target="_blank"
|
||||
>{{ gaBeian }}</a
|
||||
>
|
||||
<template v-if="icp">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank">ICP备案:{{ icp }}</a>
|
||||
</template>
|
||||
<template v-if="gaBeian">
|
||||
<span>|</span>
|
||||
<img :src="gaBeianImg" class="w-4 h-4 mx-1" alt="beian" />
|
||||
<a
|
||||
:href="`http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=${getCodeNum(
|
||||
gaBeian
|
||||
)}`"
|
||||
target="_blank"
|
||||
>{{ gaBeian }}</a
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { getLicenseInfo, getSystemInfo } from '@/store/cache'
|
||||
import { getSystemInfo } from '@/store/cache'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { ref } from 'vue'
|
||||
|
||||
@@ -36,7 +40,6 @@ const gitURL = ref(import.meta.env.VITE_GITHUB_URL)
|
||||
const copyRight = ref('')
|
||||
const icp = ref('')
|
||||
const gaBeian = ref('')
|
||||
const license = ref({})
|
||||
const props = defineProps({
|
||||
textColor: {
|
||||
type: String,
|
||||
@@ -61,14 +64,6 @@ getSystemInfo()
|
||||
showMessageError('获取系统配置失败:' + e.message)
|
||||
})
|
||||
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError('获取 License 失败:' + e.message)
|
||||
})
|
||||
|
||||
// 获取公安备案号
|
||||
const getCodeNum = (code) => {
|
||||
// 提取数字
|
||||
@@ -86,6 +81,7 @@ const getCodeNum = (code) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.foot-container {
|
||||
// 仅在 PC 端 fixed,移动端正常流式布局
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
@@ -95,6 +91,12 @@ const getCodeNum = (code) => {
|
||||
// background: var(--theme-bg);
|
||||
margin-top: -4px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-top: 2rem !important;
|
||||
position: static;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
// max-width: 400px;
|
||||
text-align: center;
|
||||
|
||||
@@ -21,15 +21,19 @@
|
||||
</div>
|
||||
<div v-else class="upload-item single-image-item">
|
||||
<el-image :src="imageList[0]" fit="cover" class="upload-image" />
|
||||
<div class="upload-overlay">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
circle
|
||||
@click="removeImage(0)"
|
||||
class="remove-btn"
|
||||
/>
|
||||
<div class="upload-overlay flex items-center justify-center space-x-2">
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<i
|
||||
class="iconfont icon-remove text-base text-red-500 cursor-pointer"
|
||||
@click="removeImage(index)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="预览" placement="top">
|
||||
<i
|
||||
class="iconfont icon-eye-open text-lg text-white cursor-pointer"
|
||||
@click="previewImage(index)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,15 +44,19 @@
|
||||
<div class="upload-list" v-if="imageList.length > 0">
|
||||
<div v-for="(image, index) in imageList" :key="index" class="upload-item">
|
||||
<el-image :src="image" fit="cover" class="upload-image" />
|
||||
<div class="upload-overlay">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
size="small"
|
||||
circle
|
||||
@click="removeImage(index)"
|
||||
class="remove-btn"
|
||||
/>
|
||||
<div class="upload-overlay flex items-center justify-center space-x-2">
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<i
|
||||
class="iconfont icon-remove text-base text-red-500 cursor-pointer"
|
||||
@click="removeImage(index)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="预览" placement="top">
|
||||
<i
|
||||
class="iconfont icon-eye-open text-lg text-white cursor-pointer"
|
||||
@click="previewImage(index)"
|
||||
></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 上传按钮 -->
|
||||
@@ -100,13 +108,20 @@
|
||||
:stroke-width="4"
|
||||
class="upload-progress"
|
||||
/>
|
||||
|
||||
<!-- 图片预览弹窗 -->
|
||||
<el-image-viewer
|
||||
v-if="previewVisible"
|
||||
:url-list="[previewImageSrc]"
|
||||
@close="previewVisible = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { replaceImg } from '@/utils/libs'
|
||||
import { Delete, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -138,6 +153,8 @@ const emit = defineEmits(['update:modelValue', 'upload-success'])
|
||||
// 上传状态
|
||||
const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const previewVisible = ref(false)
|
||||
const previewImageSrc = ref('')
|
||||
|
||||
// 图片列表
|
||||
const imageList = computed({
|
||||
@@ -225,6 +242,11 @@ const removeImage = (index) => {
|
||||
newList.splice(index, 1)
|
||||
imageList.value = newList
|
||||
}
|
||||
|
||||
const previewImage = (index) => {
|
||||
previewImageSrc.value = imageList.value[index]
|
||||
previewVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -224,11 +224,6 @@ const items = [
|
||||
index: '/admin/config/menu',
|
||||
title: '菜单配置',
|
||||
},
|
||||
{
|
||||
icon: 'license',
|
||||
index: '/admin/config/license',
|
||||
title: '授权激活',
|
||||
},
|
||||
{
|
||||
icon: 'recharge',
|
||||
index: '/admin/config/payment',
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<AttachmentList v-if="files && files.length" :files="files" />
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="action-buttons" v-if="showActions && orgContent">
|
||||
<van-button
|
||||
@@ -50,7 +54,8 @@ import emoji from 'markdown-it-emoji'
|
||||
import mathjaxPlugin from 'markdown-it-mathjax3'
|
||||
import { showImagePreview, showNotify } from 'vant'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import Thinking from '../Thinking.vue'
|
||||
import AttachmentList from '@/components/AttachmentList.vue'
|
||||
import Thinking from '@/components/Thinking.vue'
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
@@ -90,6 +95,8 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const files = computed(() => props.content.files || [])
|
||||
|
||||
const emits = defineEmits(['regenerate'])
|
||||
const md = new MarkdownIt({
|
||||
breaks: true,
|
||||
|
||||
@@ -8,8 +8,17 @@
|
||||
isImage(f.ext) ? 'p-0' : 'p-2',
|
||||
]"
|
||||
>
|
||||
<div v-if="isImage(f.ext)" class="relative w-[56px] h-[56px] overflow-hidden rounded-lg">
|
||||
<img :src="f.url" alt="img" class="w-full h-full object-cover" />
|
||||
<div
|
||||
v-if="isImage(f.ext)"
|
||||
class="relative max-w-[200px] max-h-[200px] overflow-hidden rounded-lg"
|
||||
>
|
||||
<el-image
|
||||
:src="f.url"
|
||||
alt="img"
|
||||
class="w-full h-full"
|
||||
:preview-src-list="[f.url]"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
<div v-else :class="['flex items-center', direction === 'col' ? 'w-full' : 'max-w-[240px]']">
|
||||
<img :src="GetFileIcon(f.ext)" class="w-10 h-10 mr-2" />
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-center items-center py-10 px-4 text-center">
|
||||
<div class="mb-4 flex items-center justify-center">
|
||||
<div class="w-14 h-14 rounded-full bg-gray-100 flex items-center justify-center shadow">
|
||||
<svg class="w-7 h-7 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 11c1.657 0 3-1.567 3-3.5S13.657 4 12 4 9 5.567 9 7.5 10.343 11 12 11z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 20v-1a5 5 0 015-5h2a5 5 0 015 5v1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-1">{{ title }}</h3>
|
||||
<p class="text-gray-500 text-sm">{{ statusText }}</p>
|
||||
<p v-if="description" class="text-xs text-gray-400 mt-2 max-w-sm leading-relaxed">
|
||||
{{ description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 允许使用插槽扩展,比如放置返回按钮或客服入口 -->
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '功能暂未开放',
|
||||
},
|
||||
statusText: {
|
||||
type: String,
|
||||
default: '该功能暂不可用,请稍后重试或联系管理员',
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 保留最小样式,去除无关动画与进度条 */
|
||||
</style>
|
||||
+222
-210
@@ -6,6 +6,112 @@
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { getSystemInfo, getMenus } from '@/store/cache'
|
||||
|
||||
// PC 端主页面路由
|
||||
const homeRoutes = {
|
||||
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-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-sd',
|
||||
path: '/sd',
|
||||
meta: { title: 'stable diffusion 绘画中心' },
|
||||
component: () => import('@/views/ImageSd.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: 'images',
|
||||
path: '/images-wall',
|
||||
meta: { title: '作品展示' },
|
||||
component: () => import('@/views/ImagesWall.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: 'xmind',
|
||||
path: '/xmind',
|
||||
meta: { title: '思维导图' },
|
||||
component: () => import('@/views/MarkMap.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: 'ExternalLink',
|
||||
path: '/external',
|
||||
component: () => import('@/views/ExternalPage.vue'),
|
||||
},
|
||||
{
|
||||
name: 'song',
|
||||
path: '/song/:id',
|
||||
meta: { title: 'Suno音乐播放' },
|
||||
component: () => import('@/views/Song.vue'),
|
||||
},
|
||||
{
|
||||
name: 'video',
|
||||
path: '/video',
|
||||
meta: { title: '视频创作中心' },
|
||||
component: () => import('@/views/Video.vue'),
|
||||
},
|
||||
{
|
||||
name: 'jimeng',
|
||||
path: '/jimeng',
|
||||
meta: { title: '即梦AI' },
|
||||
component: () => import('@/views/Jimeng.vue'),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -14,109 +120,6 @@ const routes = [
|
||||
meta: { title: '首页' },
|
||||
component: () => import('@/views/Index.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-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-sd',
|
||||
path: '/sd',
|
||||
meta: { title: 'stable diffusion 绘画中心' },
|
||||
component: () => import('@/views/ImageSd.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: 'images',
|
||||
path: '/images-wall',
|
||||
meta: { title: '作品展示' },
|
||||
component: () => import('@/views/ImagesWall.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: 'xmind',
|
||||
path: '/xmind',
|
||||
meta: { title: '思维导图' },
|
||||
component: () => import('@/views/MarkMap.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: 'ExternalLink',
|
||||
path: '/external',
|
||||
component: () => import('@/views/ExternalPage.vue'),
|
||||
},
|
||||
{
|
||||
name: 'song',
|
||||
path: '/song/:id',
|
||||
meta: { title: 'Suno音乐播放' },
|
||||
component: () => import('@/views/Song.vue'),
|
||||
},
|
||||
{
|
||||
name: 'video',
|
||||
path: '/video',
|
||||
meta: { title: '视频创作中心' },
|
||||
component: () => import('@/views/Video.vue'),
|
||||
},
|
||||
{
|
||||
name: 'jimeng',
|
||||
path: '/jimeng',
|
||||
meta: { title: '即梦AI' },
|
||||
component: () => import('@/views/Jimeng.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'chat-export',
|
||||
path: '/chat/export',
|
||||
@@ -240,12 +243,6 @@ const routes = [
|
||||
meta: { title: '菜单配置' },
|
||||
component: () => import('@/views/admin/settings/MenuConfig.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/config/license',
|
||||
name: 'admin-config-license',
|
||||
meta: { title: '授权激活' },
|
||||
component: () => import('@/views/admin/settings/LicenseConfig.vue'),
|
||||
},
|
||||
{
|
||||
path: '/admin/user',
|
||||
name: 'admin-user',
|
||||
@@ -351,106 +348,6 @@ const routes = [
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
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'),
|
||||
},
|
||||
{
|
||||
meta: { title: 'AI对话' },
|
||||
path: '/mobile/chat',
|
||||
name: 'mobile-chat',
|
||||
component: () => import('@/views/mobile/ChatList.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '创作中心' },
|
||||
path: '/mobile/create',
|
||||
name: 'mobile-create',
|
||||
component: () => import('@/views/mobile/Create.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '发现' },
|
||||
path: '/mobile/discover',
|
||||
name: 'mobile-discover',
|
||||
component: () => import('@/views/mobile/Discover.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '个人中心' },
|
||||
path: '/mobile/profile',
|
||||
name: 'mobile-profile',
|
||||
component: () => import('@/views/mobile/Profile.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '会员充值' },
|
||||
path: '/mobile/member',
|
||||
name: 'mobile-member',
|
||||
component: () => import('@/views/mobile/Member.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '作品展示' },
|
||||
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'),
|
||||
},
|
||||
|
||||
{
|
||||
meta: { title: '应用中心' },
|
||||
path: '/mobile/apps',
|
||||
name: 'mobile-apps',
|
||||
component: () => import('@/views/mobile/Apps.vue'),
|
||||
},
|
||||
// 新增的功能页面路由
|
||||
{
|
||||
meta: { title: '消费日志' },
|
||||
path: '/mobile/power-log',
|
||||
name: 'mobile-power-log',
|
||||
component: () => import('@/views/mobile/PowerLog.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '推广计划' },
|
||||
path: '/mobile/invite',
|
||||
name: 'mobile-invite',
|
||||
component: () => import('@/views/mobile/Invite.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '设置' },
|
||||
path: '/mobile/settings',
|
||||
name: 'mobile-settings',
|
||||
component: () => import('@/views/mobile/Settings.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: 'Suno音乐创作' },
|
||||
path: '/mobile/suno',
|
||||
name: 'mobile-suno',
|
||||
component: () => import('@/views/mobile/SunoCreate.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '视频生成' },
|
||||
path: '/mobile/video',
|
||||
name: 'mobile-video',
|
||||
component: () => import('@/views/mobile/VideoCreate.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '即梦AI' },
|
||||
path: '/mobile/jimeng',
|
||||
name: 'mobile-jimeng',
|
||||
component: () => import('@/views/mobile/JimengCreate.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'test',
|
||||
path: '/test',
|
||||
@@ -466,6 +363,106 @@ const routes = [
|
||||
},
|
||||
]
|
||||
|
||||
const mobileRoutes = {
|
||||
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'),
|
||||
},
|
||||
{
|
||||
meta: { title: 'AI对话' },
|
||||
path: '/mobile/chat',
|
||||
name: 'mobile-chat',
|
||||
component: () => import('@/views/mobile/ChatList.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '创作中心' },
|
||||
path: '/mobile/create',
|
||||
name: 'mobile-create',
|
||||
component: () => import('@/views/mobile/Create.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '发现' },
|
||||
path: '/mobile/discover',
|
||||
name: 'mobile-discover',
|
||||
component: () => import('@/views/mobile/Discover.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '个人中心' },
|
||||
path: '/mobile/profile',
|
||||
name: 'mobile-profile',
|
||||
component: () => import('@/views/mobile/Profile.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '会员充值' },
|
||||
path: '/mobile/member',
|
||||
name: 'mobile-member',
|
||||
component: () => import('@/views/mobile/Member.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '作品展示' },
|
||||
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'),
|
||||
},
|
||||
|
||||
{
|
||||
meta: { title: '应用中心' },
|
||||
path: '/mobile/apps',
|
||||
name: 'mobile-apps',
|
||||
component: () => import('@/views/mobile/Apps.vue'),
|
||||
},
|
||||
// 新增的功能页面路由
|
||||
{
|
||||
meta: { title: '消费日志' },
|
||||
path: '/mobile/power-log',
|
||||
name: 'mobile-power-log',
|
||||
component: () => import('@/views/mobile/PowerLog.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '推广计划' },
|
||||
path: '/mobile/invite',
|
||||
name: 'mobile-invite',
|
||||
component: () => import('@/views/mobile/Invite.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '设置' },
|
||||
path: '/mobile/settings',
|
||||
name: 'mobile-settings',
|
||||
component: () => import('@/views/mobile/Settings.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: 'Suno音乐创作' },
|
||||
path: '/mobile/suno',
|
||||
name: 'mobile-suno',
|
||||
component: () => import('@/views/mobile/SunoCreate.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '视频生成' },
|
||||
path: '/mobile/video',
|
||||
name: 'mobile-video',
|
||||
component: () => import('@/views/mobile/VideoCreate.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '即梦AI' },
|
||||
path: '/mobile/jimeng',
|
||||
name: 'mobile-jimeng',
|
||||
component: () => import('@/views/mobile/JimengCreate.vue'),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// console.log(MY_VARIABLE)
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -480,4 +477,19 @@ router.beforeEach((to, from, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
export { prevRoute, router }
|
||||
// 检测是否启用了移动端(同步:顶层 await)
|
||||
const res = await getSystemInfo()
|
||||
const data = res.data
|
||||
if (data && data.enable_mobile_site) {
|
||||
router.addRoute(mobileRoutes)
|
||||
}
|
||||
|
||||
// 获取所有的菜单列表,然后把禁用的菜单从router中移除
|
||||
const menus = await getMenus()
|
||||
homeRoutes.children = homeRoutes.children.filter((route) => {
|
||||
return !menus[route.path] || menus[route.path]?.enabled
|
||||
})
|
||||
// 添加主页面路由
|
||||
router.addRoute(homeRoutes)
|
||||
|
||||
export { prevRoute, router, mobileRoutes }
|
||||
|
||||
+69
-65
@@ -1,99 +1,103 @@
|
||||
import { httpGet } from "@/utils/http";
|
||||
import Storage from "good-storage";
|
||||
import { randString } from "@/utils/libs";
|
||||
import { httpGet } from '@/utils/http'
|
||||
import Storage from 'good-storage'
|
||||
import { randString } from '@/utils/libs'
|
||||
|
||||
const userDataKey = "USER_INFO_CACHE_KEY";
|
||||
const adminDataKey = "ADMIN_INFO_CACHE_KEY";
|
||||
const systemInfoKey = "SYSTEM_INFO_CACHE_KEY";
|
||||
const licenseInfoKey = "LICENSE_INFO_CACHE_KEY";
|
||||
export function checkSession() {
|
||||
const item = Storage.get(userDataKey) ?? { expire: 0, data: null };
|
||||
const userDataKey = 'USER_INFO_CACHE_KEY'
|
||||
const adminDataKey = 'ADMIN_INFO_CACHE_KEY'
|
||||
const systemInfoKey = 'SYSTEM_INFO_CACHE_KEY'
|
||||
const menusKey = 'MENUS_CACHE_KEY'
|
||||
|
||||
export function getMenus() {
|
||||
const item = Storage.get(menusKey) ?? { expire: 0, data: null }
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/user/session")
|
||||
httpGet('/api/menu/list/all')
|
||||
.then((res) => {
|
||||
item.data = res.data;
|
||||
item.expire = Date.now() + 1000 * 3;
|
||||
Storage.set(userDataKey, item);
|
||||
resolve(item.data);
|
||||
const menus = {}
|
||||
for (const menu of res.data) {
|
||||
menus[menu.url] = menu
|
||||
}
|
||||
item.data = menus
|
||||
item.expire = Date.now() + 1000 * 3
|
||||
Storage.set(menusKey, item)
|
||||
resolve(item.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function checkSession() {
|
||||
const item = Storage.get(userDataKey) ?? { expire: 0, data: null }
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet('/api/user/session')
|
||||
.then((res) => {
|
||||
item.data = res.data
|
||||
item.expire = Date.now() + 1000 * 3
|
||||
Storage.set(userDataKey, item)
|
||||
resolve(item.data)
|
||||
})
|
||||
.catch((e) => {
|
||||
Storage.remove(userDataKey);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
Storage.remove(userDataKey)
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
export function checkAdminSession() {
|
||||
const item = Storage.get(adminDataKey) ?? { expire: 0, data: null };
|
||||
const item = Storage.get(adminDataKey) ?? { expire: 0, data: null }
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/admin/session")
|
||||
httpGet('/api/admin/session')
|
||||
.then((res) => {
|
||||
item.data = res.data;
|
||||
item.expire = Date.now() + 1000 * 30;
|
||||
Storage.set(adminDataKey, item);
|
||||
resolve(item.data);
|
||||
item.data = res.data
|
||||
item.expire = Date.now() + 1000 * 3
|
||||
Storage.set(adminDataKey, item)
|
||||
resolve(item.data)
|
||||
})
|
||||
.catch((e) => {
|
||||
Storage.remove(adminDataKey);
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
Storage.remove(adminDataKey)
|
||||
reject(e)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function removeAdminInfo() {
|
||||
Storage.remove(adminDataKey);
|
||||
Storage.remove(adminDataKey)
|
||||
}
|
||||
|
||||
export function getSystemInfo() {
|
||||
const item = Storage.get(systemInfoKey) ?? { expire: 0, data: null };
|
||||
const item = Storage.get(systemInfoKey) ?? { expire: 0, data: null }
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
return Promise.resolve(item.data)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/config/get?key=system")
|
||||
httpGet('/api/config/get?key=system')
|
||||
.then((res) => {
|
||||
item.data = res;
|
||||
item.expire = Date.now() + 1000 * 30;
|
||||
Storage.set(systemInfoKey, item);
|
||||
resolve(item.data);
|
||||
item.data = res
|
||||
item.expire = Date.now() + 1000 * 3
|
||||
Storage.set(systemInfoKey, item)
|
||||
resolve(item.data)
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getLicenseInfo() {
|
||||
const item = Storage.get(licenseInfoKey) ?? { expire: 0, data: null };
|
||||
if (item.expire > Date.now()) {
|
||||
return Promise.resolve(item.data);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
httpGet("/api/config/license")
|
||||
.then((res) => {
|
||||
item.data = res;
|
||||
item.expire = Date.now() + 1000 * 30;
|
||||
Storage.set(licenseInfoKey, item);
|
||||
resolve(item.data);
|
||||
reject(err)
|
||||
})
|
||||
.catch((err) => {
|
||||
resolve(err);
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
export function getClientId() {
|
||||
let clientId = Storage.get("client_id");
|
||||
let clientId = Storage.get('client_id')
|
||||
if (clientId) {
|
||||
return clientId;
|
||||
return clientId
|
||||
}
|
||||
clientId = randString(42);
|
||||
Storage.set("client_id", clientId);
|
||||
return clientId;
|
||||
clientId = randString(42)
|
||||
Storage.set('client_id', clientId)
|
||||
return clientId
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export const JimengParams = {
|
||||
type: 'image',
|
||||
required: false,
|
||||
placeholder: '请上传图片',
|
||||
maxSize: 10,
|
||||
maxSize: 15,
|
||||
multiple: true,
|
||||
maxCount: 10,
|
||||
accept: '.png,.jpg,.jpeg',
|
||||
|
||||
@@ -71,6 +71,7 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
// 获取任务状态文本
|
||||
const getTaskStatusText = (status) => {
|
||||
const statusMap = {
|
||||
submited: '任务已提交',
|
||||
in_queue: '任务排队中',
|
||||
generating: '任务执行中',
|
||||
success: '任务成功',
|
||||
|
||||
@@ -203,7 +203,7 @@ export function processContent(content) {
|
||||
if (content.includes('<think>')) {
|
||||
content = content.replace(/<think>(.*?)<\/think>/gs, (match, content) => {
|
||||
if (content.length > 10) {
|
||||
return `<blockquote>${content}</blockquote>`
|
||||
return `<blockquote>\n\n${content}</blockquote>`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
+74
-18
@@ -1,39 +1,95 @@
|
||||
<template>
|
||||
<div class="page-404" :style="{ height: winHeight + 'px' }">
|
||||
<div class="page-404">
|
||||
<div class="inner">
|
||||
<h1>404</h1>
|
||||
<h2>饶了地球一圈,还是没有找到您要的页面</h2>
|
||||
<div class="code">404</div>
|
||||
<p class="title">抱歉,页面走丢了</p>
|
||||
<p class="desc">您访问的页面不存在或已被移动</p>
|
||||
<div class="actions">
|
||||
<button class="btn" @click="goHome">返回首页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const winHeight = ref(window.innerHeight)
|
||||
const router = useRouter()
|
||||
function goHome() {
|
||||
router.replace({ path: '/' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-404 {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
padding: calc(env(safe-area-inset-top, 0px) + 24px) 16px
|
||||
calc(env(safe-area-inset-bottom, 0px) + 32px);
|
||||
background: radial-gradient(1200px 600px at 50% -10%, #2f3542 0%, #1e222a 60%, #12161c 100%);
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
.inner {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #202020;
|
||||
font-size: 120px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.1em;
|
||||
text-shadow: -1px -1px 1px #111111, 2px 2px 1px #363636;
|
||||
}
|
||||
.code {
|
||||
font-size: clamp(96px, 18vw, 160px);
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.04em;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #cbd5e1 60%, #94a3b8 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.title {
|
||||
margin-top: 16px;
|
||||
font-size: clamp(18px, 4.6vw, 28px);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 8px;
|
||||
font-size: clamp(14px, 3.6vw, 16px);
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
border-radius: 9999px;
|
||||
padding: 12px 20px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #ffffff;
|
||||
background: #4f46e5;
|
||||
box-shadow: 0 10px 20px rgba(79, 70, 229, 0.35), inset 0 0 0 1px rgba(255, 255, 255, 0.15);
|
||||
cursor: pointer;
|
||||
transition: transform 0.12s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #4338ca;
|
||||
box-shadow: 0 12px 24px rgba(79, 70, 229, 0.45), inset 0 0 0 1px rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -140,14 +140,9 @@
|
||||
plain
|
||||
>
|
||||
<div class="selected-model-display">
|
||||
<span class="model-name-text">{{ getSelectedModelName() }}</span>
|
||||
<el-tag
|
||||
v-if="getSelectedModel()"
|
||||
size="small"
|
||||
type="info"
|
||||
style="margin-left: 8px; flex-shrink: 0"
|
||||
>
|
||||
{{ getSelectedModel() && getSelectedModel().power }}算力
|
||||
<span class="model-name-text">{{ selectedModel.name }}</span>
|
||||
<el-tag size="small" type="info" style="margin-left: 8px; flex-shrink: 0">
|
||||
{{ selectedModel.power }}算力
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-button>
|
||||
@@ -546,10 +541,10 @@ watch(
|
||||
)
|
||||
|
||||
// 获取选中的模型名称
|
||||
const getSelectedModelName = () => {
|
||||
const selectedModel = computed(() => {
|
||||
const model = getSelectedModel()
|
||||
return model ? model.name : '选择模型'
|
||||
}
|
||||
return model ? model : { name: '选择模型', power: 0 }
|
||||
})
|
||||
|
||||
// 获取选中的模型
|
||||
const getSelectedModel = () => {
|
||||
@@ -585,10 +580,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
if (isMobile()) {
|
||||
router.push('/mobile/chat')
|
||||
}
|
||||
|
||||
// 初始化角色ID参数
|
||||
if (router.currentRoute.value.query.role_id) {
|
||||
roleId.value = parseInt(router.currentRoute.value.query.role_id)
|
||||
@@ -666,7 +657,7 @@ const initData = async () => {
|
||||
// 获取模型列表
|
||||
const modelRes = await httpGet('/api/model/list')
|
||||
models.value = modelRes.data
|
||||
if (models.value.length > 0) {
|
||||
if (models.value.length > 0 && !modelID.value) {
|
||||
modelID.value = models.value[0].id
|
||||
}
|
||||
|
||||
|
||||
+71
-69
@@ -3,7 +3,7 @@
|
||||
<div class="page-dall">
|
||||
<div class="inner custom-scroll">
|
||||
<div class="sd-box">
|
||||
<h2>DALL-E 创作中心</h2>
|
||||
<h2 class="!text-[#252f76] py-3">AI图像生成</h2>
|
||||
|
||||
<div class="sd-params">
|
||||
<el-form :model="params" label-width="80px" label-position="left">
|
||||
@@ -25,55 +25,25 @@
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片质量">
|
||||
<el-form-item label="图片比例">
|
||||
<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-select>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片尺寸">
|
||||
<template #default>
|
||||
<div class="form-item-inner">
|
||||
<el-select v-model="params.size" style="width: 150px">
|
||||
<el-option v-for="v in sizes" :label="v" :value="v" :key="v" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="param-line">
|
||||
<el-form-item label="图片样式">
|
||||
<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-select>
|
||||
<el-tooltip
|
||||
content="生动使模型倾向于生成超真实和戏剧性的图像"
|
||||
raw-content
|
||||
placement="right"
|
||||
<el-select
|
||||
v-model="params.aspect_ratio"
|
||||
style="width: 150px"
|
||||
value-key="value"
|
||||
@change="changeAspectRatio"
|
||||
>
|
||||
<el-icon class="info-icon">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-option
|
||||
v-for="v in radioAspects"
|
||||
:label="v.label"
|
||||
:value="v.value"
|
||||
:key="v.value"
|
||||
>
|
||||
<i class="iconfont mr-1" :class="v.icon"></i>
|
||||
{{ v.label }}
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
@@ -105,7 +75,12 @@
|
||||
<div class="mt-2 mb-2">
|
||||
<label class="text-gray-700 font-semibold">参考图(可选)</label>
|
||||
<div class="py-2">
|
||||
<ImageUpload v-model="params.image" :max-count="5" :multiple="true" />
|
||||
<ImageUpload
|
||||
v-model="params.image"
|
||||
:max-count="5"
|
||||
:max-size="20"
|
||||
:multiple="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -291,18 +266,18 @@
|
||||
import nodata from '@/assets/img/no-data.png'
|
||||
|
||||
import BackTop from '@/components/BackTop.vue'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
import TaskList from '@/components/TaskList.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { Delete, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { LazyImg, Waterfall } from 'vue-waterfall-plugin-next'
|
||||
import 'vue-waterfall-plugin-next/dist/style.css'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
|
||||
const listBoxHeight = ref(0)
|
||||
// const paramBoxHeight = ref(0)
|
||||
@@ -321,21 +296,48 @@ 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' },
|
||||
]
|
||||
|
||||
// 为了兼容 size 和 aspect_ratio 参数,label 对应的是 aspect_ratio 的值,size 是预估值
|
||||
const radioAspects = ref({
|
||||
'1:1': { value: '1:1', size: '1024x1024', label: '1:1(正方形,头像)', icon: 'icon-aspect_1_1' },
|
||||
'2:3': {
|
||||
value: '2:3',
|
||||
size: '512x768',
|
||||
label: '2:3(社交媒体,自拍)',
|
||||
icon: 'icon-aspect_2_3',
|
||||
},
|
||||
'4:3': {
|
||||
value: '4:3',
|
||||
size: '1024x768',
|
||||
label: '4:3(文章配图,插画)',
|
||||
icon: 'icon-aspect_4_3',
|
||||
},
|
||||
'9:16': {
|
||||
value: '9:16',
|
||||
size: '768x1366',
|
||||
label: '9:16(手机壁纸,人像)',
|
||||
icon: 'icon-aspect_9_16',
|
||||
},
|
||||
'16:9': {
|
||||
value: '16:9',
|
||||
size: '1366x768',
|
||||
label: '16:9(桌面壁纸,风景)',
|
||||
icon: 'icon-aspect_16_9',
|
||||
},
|
||||
'3:2': { value: '3:2', size: '768x512', label: '3:2(1248x832)', icon: 'icon-aspect_3_2' },
|
||||
'3:4': { value: '3:4', size: '960x1280', label: '3:4(864x1152)', icon: 'icon-aspect_3_4' },
|
||||
'4:5': { value: '4:5', size: '960x1280', label: '4:5(896x1120)', icon: 'icon-aspect_4_5' },
|
||||
'5:4': {
|
||||
value: '5:4',
|
||||
size: '1280x960',
|
||||
label: '5:4(1120x896)',
|
||||
icon: 'icon-aspect_5_4',
|
||||
},
|
||||
'21:9': { value: '21:9', size: '1344x576', label: '21:9(1536x658)', icon: 'icon-aspect_16_9' },
|
||||
})
|
||||
const params = ref({
|
||||
quality: 'standard',
|
||||
aspect_ratio: '1:1',
|
||||
size: '1024x1024',
|
||||
style: 'vivid',
|
||||
prompt: '',
|
||||
})
|
||||
|
||||
@@ -385,6 +387,11 @@ onUnmounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 改变图片比例
|
||||
const changeAspectRatio = (value) => {
|
||||
params.value.size = radioAspects.value[value].size
|
||||
}
|
||||
|
||||
const initData = () => {
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
@@ -580,11 +587,6 @@ const generatePrompt = () => {
|
||||
|
||||
const changeModel = (model) => {
|
||||
dallPower.value = model.power
|
||||
if (model.name.startsWith('dall')) {
|
||||
sizes.value = dalleSizes
|
||||
} else {
|
||||
sizes.value = fluxSizes
|
||||
}
|
||||
params.value.model_id = selectedModel.value.id
|
||||
}
|
||||
</script>
|
||||
|
||||
+2
-12
@@ -82,7 +82,7 @@
|
||||
<span class="username title">账户信息</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!license.de_copy">
|
||||
<li>
|
||||
<a :href="githubURL" target="_blank" class="flex">
|
||||
<i class="iconfont icon-github"></i>
|
||||
<span class="title">项目源码</span>
|
||||
@@ -150,10 +150,9 @@
|
||||
import LoginDialog from '@/components/LoginDialog.vue'
|
||||
import ThemeChange from '@/components/ThemeChange.vue'
|
||||
import ConfigDialog from '@/components/UserInfoDialog.vue'
|
||||
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { removeUserToken } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { showMessageError } from '@/utils/dialog'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -171,7 +170,6 @@ 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(import.meta.env.VITE_GITHUB_URL)
|
||||
|
||||
@@ -261,14 +259,6 @@ onMounted(() => {
|
||||
ElMessage.error('获取系统菜单失败:' + e.message)
|
||||
})
|
||||
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
license.value = { de_copy: false }
|
||||
showMessageError('获取 License 配置:' + e.message)
|
||||
})
|
||||
curPath.value = '/' + getFirstPathSegment(window.location.href)
|
||||
init()
|
||||
})
|
||||
|
||||
+6
-18
@@ -8,7 +8,7 @@
|
||||
<img :src="logo" class="logo" alt="Geek-AI" />
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<span v-if="!license || !license.de_copy">
|
||||
<span>
|
||||
<el-tooltip class="box-item" content="部署文档" placement="bottom">
|
||||
<a :href="docsURL" class="link-button mr-3" target="_blank">
|
||||
<i class="iconfont icon-book"></i>
|
||||
@@ -88,7 +88,7 @@
|
||||
<script setup>
|
||||
import FooterBar from '@/components/FooterBar.vue'
|
||||
import ThemeChange from '@/components/ThemeChange.vue'
|
||||
import { checkSession, getLicenseInfo, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { removeUserToken } from '@/store/session'
|
||||
import { httpGet } from '@/utils/http'
|
||||
import { isMobile } from '@/utils/libs'
|
||||
@@ -102,7 +102,6 @@ const router = useRouter()
|
||||
|
||||
const title = ref('')
|
||||
const logo = ref('')
|
||||
const license = ref({ de_copy: true })
|
||||
|
||||
const isLogin = ref(false)
|
||||
const docsURL = ref(import.meta.env.VITE_DOCS_URL)
|
||||
@@ -123,17 +122,15 @@ const md = new MarkdownIt({
|
||||
typographer: true,
|
||||
}).use(emoji)
|
||||
|
||||
if (isMobile()) {
|
||||
router.push('/mobile/index')
|
||||
}
|
||||
|
||||
getSystemInfo()
|
||||
.then((res) => {
|
||||
const data = res.data
|
||||
title.value = data.title
|
||||
logo.value = data.logo
|
||||
console.log(data)
|
||||
if (data.index_page) {
|
||||
|
||||
if (isMobile() && data.enable_mobile_site) {
|
||||
router.push('/mobile/index')
|
||||
} else if (data.index_page) {
|
||||
router.push(data.index_page)
|
||||
}
|
||||
})
|
||||
@@ -142,15 +139,6 @@ getSystemInfo()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getLicenseInfo()
|
||||
.then((res) => {
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
license.value = { de_copy: false }
|
||||
ElMessage.error('获取 License 配置失败:' + e.message)
|
||||
})
|
||||
|
||||
httpGet('/api/menu/list?index=1')
|
||||
.then((res) => {
|
||||
navs.value = res.data
|
||||
|
||||
@@ -229,7 +229,6 @@
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="task-item">
|
||||
<!-- 保持原有内容 -->
|
||||
<div class="task-left">
|
||||
<div class="task-preview">
|
||||
<el-image
|
||||
|
||||
@@ -41,7 +41,8 @@
|
||||
<el-table-column prop="type" label="模型类型">
|
||||
<template #default="scope">
|
||||
<el-tag type="primary" v-if="scope.row.type === 'img'">绘图</el-tag>
|
||||
<el-tag type="success" v-else>聊天</el-tag>
|
||||
<el-tag type="warning" v-if="scope.row.type === 'tts'">语音</el-tag>
|
||||
<el-tag type="success" v-if="scope.row.type === 'chat'">聊天</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -230,7 +231,7 @@
|
||||
<script setup>
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { dateFormat, removeArrayItem, substr } from '@/utils/libs'
|
||||
import { DocumentCopy, InfoFilled, Plus, Search, Delete } from '@element-plus/icons-vue'
|
||||
import { Delete, DocumentCopy, InfoFilled, Plus, Search } from '@element-plus/icons-vue'
|
||||
import ClipboardJS from 'clipboard'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Sortable } from 'sortablejs'
|
||||
@@ -239,7 +240,7 @@ import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
// 变量定义
|
||||
const items = ref([])
|
||||
const query = ref({ name: '' })
|
||||
const item = ref({})
|
||||
const item = ref({ options: {} })
|
||||
const showDialog = ref(false)
|
||||
const title = ref('')
|
||||
const rules = reactive({
|
||||
@@ -353,6 +354,7 @@ const add = function () {
|
||||
max_tokens: 1024,
|
||||
max_context: 8192,
|
||||
temperature: 0.9,
|
||||
options: {},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<el-switch v-model="scope.row.enabled" @change="functionSet('enabled', scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="消耗算力" prop="power" />
|
||||
|
||||
<el-table-column label="操作" width="150" align="right">
|
||||
<template #default="scope">
|
||||
@@ -128,6 +129,9 @@
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="item.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="消耗算力" prop="power">
|
||||
<el-input-number v-model.number="item.power" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
|
||||
@@ -138,12 +138,12 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="聊天角色" prop="chat_roles">
|
||||
<el-form-item label="应用权限" prop="chat_roles">
|
||||
<el-select
|
||||
v-model="user.chat_roles"
|
||||
multiple
|
||||
:filterable="true"
|
||||
placeholder="选择聊天角色,多选"
|
||||
placeholder="选择 AI 应用"
|
||||
>
|
||||
<el-option v-for="item in roles" :key="item.key" :label="item.name" :value="item.key" />
|
||||
</el-select>
|
||||
@@ -243,8 +243,6 @@ const rules = reactive({
|
||||
{ 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)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="版权信息" prop="copyright">
|
||||
<el-input v-model="system['copyright']" placeholder="更改此选项需要获取 License 授权" />
|
||||
<el-input v-model="system['copyright']" placeholder="请输入版权信息" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="ICP 备案号" prop="icp">
|
||||
@@ -251,7 +251,19 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="label-title">
|
||||
启用手机站
|
||||
<span class="text-xs text-gray-500"
|
||||
>(启用手机站点之后,用户可以在手机上访问网站)</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="system['enable_mobile_site']" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="py-2">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</el-form-item>
|
||||
@@ -323,6 +335,12 @@ const save = function () {
|
||||
if (valid) {
|
||||
httpPost('/api/admin/config/update/base', system.value)
|
||||
.then(() => {
|
||||
// 动态更新手机站路由
|
||||
if (system.value['enable_mobile_site']) {
|
||||
// addMobileRoutes()
|
||||
} else {
|
||||
// removeMobileRoutes()
|
||||
}
|
||||
ElMessage.success('操作成功!')
|
||||
})
|
||||
.catch((e) => {
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
<template>
|
||||
<div class="license-config form p-5" v-loading="loading">
|
||||
<div class="container">
|
||||
<el-descriptions
|
||||
v-if="license.is_active"
|
||||
class="margin-top"
|
||||
title="已授权信息"
|
||||
:column="1"
|
||||
border
|
||||
>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">License Key</div>
|
||||
</template>
|
||||
{{ license.key }}
|
||||
<el-tooltip content="复制" placement="top">
|
||||
<i class="iconfont icon-copy ml-2 cursor-pointer" @click="copy(license.key)"></i>
|
||||
</el-tooltip>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">机器码</div>
|
||||
</template>
|
||||
{{ license.machine_id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">到期时间</div>
|
||||
</template>
|
||||
{{ dateFormat(license.expired_at) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">用户人数</div>
|
||||
</template>
|
||||
{{ license.configs?.user_num }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">去版权</div>
|
||||
</template>
|
||||
<el-icon class="selected" v-if="license.configs?.de_copy"><Select /></el-icon>
|
||||
<el-icon class="closed" v-else><CloseBold /></el-icon>
|
||||
<span class="text">去版权之后前端页面将不会显示版权信息和源码地址</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="mt-5 p-5 border border-gray-200 rounded-md bg-gray-50">
|
||||
<h3>激活后可获得以下权限:</h3>
|
||||
<div class="py-3 text-gray-500 leading-relaxed">
|
||||
<p>1、使用任意第三方中转 API KEY,而不用局限于 GeekAI 推荐的白名单列表</p>
|
||||
<p>2、可以在相关页面去除 GeekAI 的版权信息,或者修改为自己的版权信息</p>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="许可授权码" prop="license">
|
||||
<el-input v-model="licenseKey" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="active">立即激活</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { dateFormat } from '@/utils/libs'
|
||||
import { CloseBold, Select } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const license = ref({ is_active: false })
|
||||
const licenseKey = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
fetchLicense()
|
||||
})
|
||||
|
||||
const fetchLicense = () => {
|
||||
httpGet('/api/admin/config/license/get')
|
||||
.then((res) => {
|
||||
license.value = res.data
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('获取 License 失败:' + e.message)
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 激活授权
|
||||
const active = () => {
|
||||
if (licenseKey.value === '') {
|
||||
return ElMessage.error('请输入授权码')
|
||||
}
|
||||
httpPost('/api/admin/config/license/active', { license: licenseKey.value })
|
||||
.then((res) => {
|
||||
ElMessage.success('授权成功,机器编码为:' + res.data)
|
||||
fetchLicense()
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const copy = (text) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
ElMessage.success('复制成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.license-config {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
padding: 10px 20px 40px 20px;
|
||||
}
|
||||
|
||||
.margin-top {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.cell-item {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.selected {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.closed {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.active-info {
|
||||
margin: 20px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.active-info li {
|
||||
margin: 10px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -37,7 +37,7 @@
|
||||
<i class="iconfont icon-info"></i>
|
||||
</el-tooltip>
|
||||
</label>
|
||||
<el-input v-model="smtpConfig.port" type="number" placeholder="请输入端口号" />
|
||||
<el-input v-model.number="smtpConfig.port" type="number" placeholder="请输入端口号" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="是否使用TLS"
|
||||
|
||||
@@ -38,14 +38,14 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="MinIO" name="minio">
|
||||
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/oss.html#%E6%90%AD%E5%BB%BA-minio-%E5%AD%98%E5%82%A8%E6%9C%8D%E5%8A%A1"
|
||||
target="_blank"
|
||||
>Minio 配置</a
|
||||
>。
|
||||
</div>
|
||||
</Alert>
|
||||
<el-form :model="minio" class="mt-4" label-position="top">
|
||||
<el-form-item label="Endpoint"><el-input v-model="minio.endpoint" /></el-form-item>
|
||||
<el-form-item label="AccessKey"><el-input v-model="minio.access_key" /></el-form-item>
|
||||
@@ -59,14 +59,14 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="七牛云" name="qiniu">
|
||||
<div class="rounded-md bg-blue-100 p-3 text-gray-500 border-blue-500 border-2 text-base">
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/oss.html#%E4%B8%83%E7%89%9B%E4%BA%91-oss-%E9%85%8D%E7%BD%AE"
|
||||
target="_blank"
|
||||
>七牛云配置</a
|
||||
>。
|
||||
</div>
|
||||
</Alert>
|
||||
<el-form :model="qiniu" class="mt-4" label-position="top">
|
||||
<el-form-item label="Zone">
|
||||
<template #label>
|
||||
@@ -96,6 +96,14 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="阿里云OSS" name="aliyun">
|
||||
<Alert type="info">
|
||||
如果你不知道怎么获取这些配置信息,请参考文档:
|
||||
<a
|
||||
href="https://docs.geekai.me/plus/config/oss.html#%E9%98%BF%E9%87%8C%E4%BA%91-oss-%E9%85%8D%E7%BD%AE"
|
||||
target="_blank"
|
||||
>阿里云OSS配置</a
|
||||
>。
|
||||
</Alert>
|
||||
<el-form :model="aliyun" class="mt-4" label-position="top">
|
||||
<el-form-item label="Endpoint"><el-input v-model="aliyun.endpoint" /></el-form-item>
|
||||
<el-form-item label="AccessKey"><el-input v-model="aliyun.access_key" /></el-form-item>
|
||||
@@ -128,11 +136,12 @@
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import Alert from '@/components/ui/Alert.vue'
|
||||
|
||||
const loading = ref(true)
|
||||
const activeTab = ref('local')
|
||||
const active = ref('local')
|
||||
const local = ref({ base_path: '', base_url: '' })
|
||||
const local = ref({ base_path: './static/upload', base_url: '/static/upload' })
|
||||
const minio = ref({
|
||||
endpoint: '',
|
||||
access_key: '',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="app-background">
|
||||
<div class="app-background" v-if="menus['/chat']?.enabled">
|
||||
<div class="container mobile-chat-list">
|
||||
<van-nav-bar
|
||||
:title="title"
|
||||
@@ -72,15 +72,19 @@
|
||||
<van-field v-model="tmpChatTitle" label="" placeholder="请输入对话标题" class="field" />
|
||||
</van-dialog>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FunDisabled />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { checkSession, getMenus } from '@/store/cache'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { removeArrayItem, showLoginDialog } from '@/utils/libs'
|
||||
import { showConfirmDialog, showFailToast, showSuccessToast } from 'vant'
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import FunDisabled from '@/components/ui/FunDisabled.vue'
|
||||
|
||||
const title = ref('会话列表')
|
||||
const router = useRouter()
|
||||
@@ -99,50 +103,61 @@ const columns = ref([roles.value, models.value])
|
||||
const showEditChat = ref(false)
|
||||
const item = ref({})
|
||||
const tmpChatTitle = ref('')
|
||||
const menus = ref({})
|
||||
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
finished.value = true
|
||||
// 加载角色列表
|
||||
httpGet(`/api/app/list`)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
// console.log(items[i])
|
||||
roles.value.push({
|
||||
text: items[i].name,
|
||||
value: items[i].id,
|
||||
icon: items[i].icon,
|
||||
helloMsg: items[i].hello_msg,
|
||||
model_id: items[i].model_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showFailToast('加载聊天角色失败')
|
||||
})
|
||||
onMounted(() => {
|
||||
getMenus()
|
||||
.then((data) => {
|
||||
menus.value = data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取菜单失败:' + e.message })
|
||||
})
|
||||
|
||||
// 加载模型
|
||||
httpGet('/api/model/list?enable=1')
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
models.value.push({ text: items[i].name, value: items[i].id })
|
||||
checkSession()
|
||||
.then((user) => {
|
||||
loginUser.value = user
|
||||
isLogin.value = true
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
finished.value = true
|
||||
// 加载角色列表
|
||||
httpGet(`/api/app/list`)
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
// console.log(items[i])
|
||||
roles.value.push({
|
||||
text: items[i].name,
|
||||
value: items[i].id,
|
||||
icon: items[i].icon,
|
||||
helloMsg: items[i].hello_msg,
|
||||
model_id: items[i].model_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('加载模型失败: ' + e.message)
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
showFailToast('加载聊天角色失败')
|
||||
})
|
||||
|
||||
// 加载模型
|
||||
httpGet('/api/model/list?enable=1')
|
||||
.then((res) => {
|
||||
if (res.data) {
|
||||
const items = res.data
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
models.value.push({ text: items[i].name, value: items[i].id })
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
showFailToast('加载模型失败: ' + e.message)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const onLoad = () => {
|
||||
checkSession()
|
||||
|
||||
@@ -50,12 +50,6 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const activeTab = ref(route.query.tab || 'mj')
|
||||
const menus = ref([])
|
||||
const activeMenu = ref({
|
||||
mj: false,
|
||||
sd: false,
|
||||
dall: false,
|
||||
})
|
||||
|
||||
// Tab切换处理
|
||||
const onTabChange = (name) => {
|
||||
|
||||
@@ -42,8 +42,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getMenus } from '@/store/cache'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -60,6 +62,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/create?tab=mj',
|
||||
path: '/mj',
|
||||
},
|
||||
{
|
||||
key: 'sd',
|
||||
@@ -71,6 +74,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/create?tab=sd',
|
||||
path: '/sd',
|
||||
},
|
||||
{
|
||||
key: 'dalle',
|
||||
@@ -82,6 +86,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/create?tab=dalle',
|
||||
path: '/dalle',
|
||||
},
|
||||
{
|
||||
key: 'suno',
|
||||
@@ -94,6 +99,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/suno',
|
||||
path: '/suno',
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
@@ -105,6 +111,7 @@ const aiTools = ref([
|
||||
status: 'beta',
|
||||
statusText: '测试版',
|
||||
url: '/mobile/video',
|
||||
path: '/video',
|
||||
},
|
||||
{
|
||||
key: 'jimeng',
|
||||
@@ -116,6 +123,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/jimeng',
|
||||
path: '/jimeng',
|
||||
},
|
||||
{
|
||||
key: 'imgWall',
|
||||
@@ -127,6 +135,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/imgWall',
|
||||
path: '/images-wall',
|
||||
},
|
||||
{
|
||||
key: 'apps',
|
||||
@@ -138,6 +147,7 @@ const aiTools = ref([
|
||||
status: 'active',
|
||||
statusText: '可用',
|
||||
url: '/mobile/apps',
|
||||
path: '/apps',
|
||||
},
|
||||
])
|
||||
|
||||
@@ -149,6 +159,12 @@ const navigateTo = (url) => {
|
||||
router.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getMenus().then((menus) => {
|
||||
aiTools.value = aiTools.value.filter((tool) => menus[tool.path]?.enabled)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -84,7 +84,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use '../../assets/iconfont/iconfont.css' as *;
|
||||
@use '@/assets/iconfont/iconfont.css' as *;
|
||||
|
||||
.mobile-home {
|
||||
.page-content {
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo, getMenus } from '@/store/cache'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { arrayContains, removeArrayItem, showLoginDialog, substr } from '@/utils/libs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -145,6 +145,7 @@ const features = ref([
|
||||
icon: 'icon-dalle',
|
||||
color: '#F59E0B',
|
||||
url: '/mobile/create?tab=dalle',
|
||||
path: '/dalle',
|
||||
},
|
||||
{
|
||||
key: 'suno',
|
||||
@@ -152,6 +153,7 @@ const features = ref([
|
||||
icon: 'icon-mp3',
|
||||
color: '#EF4444',
|
||||
url: '/mobile/suno',
|
||||
path: '/suno',
|
||||
},
|
||||
{
|
||||
key: 'video',
|
||||
@@ -159,6 +161,7 @@ const features = ref([
|
||||
icon: 'icon-video',
|
||||
color: '#10B981',
|
||||
url: '/mobile/video',
|
||||
path: '/video',
|
||||
},
|
||||
{
|
||||
key: 'jimeng',
|
||||
@@ -166,6 +169,7 @@ const features = ref([
|
||||
icon: 'icon-jimeng',
|
||||
color: '#F97316',
|
||||
url: '/mobile/jimeng',
|
||||
path: '/jimeng',
|
||||
},
|
||||
{
|
||||
key: '3d',
|
||||
@@ -173,6 +177,7 @@ const features = ref([
|
||||
icon: 'icon-3d',
|
||||
color: '#8B5CF6',
|
||||
url: '/mobile/3d',
|
||||
path: '/3d',
|
||||
},
|
||||
{ key: 'agent', name: '智能体', icon: 'icon-app', color: '#3B82F6', url: '/mobile/apps' },
|
||||
{
|
||||
@@ -181,6 +186,7 @@ const features = ref([
|
||||
icon: 'icon-image-list',
|
||||
color: '#EC4899',
|
||||
url: '/mobile/imgWall',
|
||||
path: '/images-wall',
|
||||
},
|
||||
])
|
||||
|
||||
@@ -225,6 +231,14 @@ onMounted(() => {
|
||||
})
|
||||
.catch(() => {})
|
||||
|
||||
getMenus()
|
||||
.then((menus) => {
|
||||
features.value = features.value.filter((feature) => menus[feature.path]?.enabled)
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('获取菜单失败:' + e.message)
|
||||
})
|
||||
|
||||
fetchApps()
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="member-page">
|
||||
<div class="member-page" v-if="menus['/member']?.enabled">
|
||||
<div class="member-content" v-loading="loading" :element-loading-text="loadingText">
|
||||
<!-- 产品套餐 -->
|
||||
<div class="products-section">
|
||||
@@ -103,14 +103,18 @@
|
||||
</div>
|
||||
</van-dialog>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FunDisabled />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RedeemVerify from '@/components/RedeemVerify.vue'
|
||||
import UserOrder from '@/components/UserOrder.vue'
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo, getMenus } from '@/store/cache'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import FunDisabled from '@/components/ui/FunDisabled.vue'
|
||||
import QRCode from 'qrcode'
|
||||
import { showFailToast, showLoadingToast, showSuccessToast } from 'vant'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
@@ -123,6 +127,7 @@ const loading = ref(true)
|
||||
const loadingText = ref('加载中...')
|
||||
const vipInfoText = ref('')
|
||||
const userOrderKey = ref(0)
|
||||
const menus = ref({})
|
||||
|
||||
// 弹窗控制
|
||||
const showRedeemVerifyDialog = ref(false)
|
||||
@@ -171,6 +176,14 @@ onMounted(() => {
|
||||
.catch((e) => {
|
||||
console.error('获取系统配置失败:', e.message)
|
||||
})
|
||||
|
||||
getMenus()
|
||||
.then((data) => {
|
||||
menus.value = data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取菜单失败:' + e.message })
|
||||
})
|
||||
})
|
||||
|
||||
// 支付处理
|
||||
|
||||
@@ -1,203 +1,193 @@
|
||||
<template>
|
||||
<div class="mobile-sd">
|
||||
<van-form>
|
||||
<van-cell-group class="px-3 pt-3 pb-4">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="selectedModel"
|
||||
is-link
|
||||
label="生图模型"
|
||||
placeholder="选择生图模型"
|
||||
@click="showModelPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showModelPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="models"
|
||||
@cancel="showModelPicker = false"
|
||||
@confirm="modelConfirm"
|
||||
<div v-if="menus['/dalle']?.enabled">
|
||||
<van-form>
|
||||
<van-cell-group class="px-3 pt-3 pb-4">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="selectedModel"
|
||||
is-link
|
||||
label="生图模型"
|
||||
placeholder="选择生图模型"
|
||||
@click="showModelPicker = true"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
<div>
|
||||
<van-field
|
||||
v-model="quality"
|
||||
is-link
|
||||
label="图片质量"
|
||||
placeholder="选择图片质量"
|
||||
@click="showQualityPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showQualityPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="qualities"
|
||||
@cancel="showQualityPicker = false"
|
||||
@confirm="qualityConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<van-field
|
||||
v-model="size"
|
||||
is-link
|
||||
label="图片尺寸"
|
||||
placeholder="选择图片尺寸"
|
||||
@click="showSizePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showSizePicker" position="bottom" teleport="#app">
|
||||
<van-picker :columns="sizes" @cancel="showSizePicker = false" @confirm="sizeConfirm" />
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<van-field
|
||||
v-model="style"
|
||||
is-link
|
||||
label="图片样式"
|
||||
placeholder="选择图片样式"
|
||||
@click="showStylePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showStylePicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="styles"
|
||||
@cancel="showStylePicker = false"
|
||||
@confirm="styleConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
maxlength="2000"
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
|
||||
<div class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm">
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ dallPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
<van-popup v-model:show="showModelPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="models"
|
||||
@cancel="showModelPicker = false"
|
||||
@confirm="modelConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<van-field
|
||||
v-model="aspectRatio"
|
||||
is-link
|
||||
label="图片比例"
|
||||
placeholder="选择图片比例"
|
||||
@click="showSizePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showSizePicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="radioAspects"
|
||||
@cancel="showSizePicker = false"
|
||||
@confirm="sizeConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
maxlength="2000"
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
|
||||
<div class="mt-3 mb-3 px-3">
|
||||
<label class="text-gray-700 font-semibold">参考图(可选)</label>
|
||||
<div class="py-2">
|
||||
<ImageUpload v-model="params.image" :max-count="5" :multiple="true" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
<div
|
||||
class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm"
|
||||
>
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ dallPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle />
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage($event, item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="removeImage($event, item)"
|
||||
circle
|
||||
/>
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage($event, item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
<button
|
||||
style="display: none"
|
||||
class="copy-prompt-dall"
|
||||
:data-clipboard-text="prompt"
|
||||
id="copy-btn-dall"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FunDisabled />
|
||||
</div>
|
||||
<button
|
||||
style="display: none"
|
||||
class="copy-prompt-dall"
|
||||
:data-clipboard-text="prompt"
|
||||
id="copy-btn-dall"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo, getMenus } from '@/store/cache'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
@@ -215,52 +205,64 @@ import {
|
||||
} from 'vant'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import FunDisabled from '@/components/ui/FunDisabled.vue'
|
||||
import ImageUpload from '@/components/ImageUpload.vue'
|
||||
|
||||
const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const isLogin = ref(false)
|
||||
const menus = ref({})
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
mjBoxHeight.value = window.innerHeight - 150
|
||||
}
|
||||
|
||||
const qualities = [
|
||||
{ text: '标准', value: 'standard' },
|
||||
{ text: '高清', value: 'hd' },
|
||||
]
|
||||
const fluxSizes = [
|
||||
{ text: '1024x1024', value: '1024x1024' },
|
||||
{ text: '1024x768', value: '1024x768' },
|
||||
{ text: '768x1024', value: '768x1024' },
|
||||
{ text: '1280x960', value: '1280x960' },
|
||||
{ text: '960x1280', value: '960x1280' },
|
||||
{ text: '1366x768', value: '1366x768' },
|
||||
{ text: '768x1366', value: '768x1366' },
|
||||
]
|
||||
const dalleSizes = [
|
||||
{ text: '1024x1024', value: '1024x1024' },
|
||||
{ text: '1792x1024', value: '1792x1024' },
|
||||
{ text: '1024x1792', value: '1024x1792' },
|
||||
]
|
||||
|
||||
let sizes = dalleSizes
|
||||
const styles = [
|
||||
{ text: '生动', value: 'vivid' },
|
||||
{ text: '自然', value: 'natural' },
|
||||
]
|
||||
// 为了兼容 size 和 aspect_ratio 参数,label 对应的是 aspect_ratio 的值,size 是预估值
|
||||
const radioAspects = ref([
|
||||
{ value: '1:1', size: '1024x1024', text: '1:1(正方形,头像)', icon: 'icon-aspect_1_1' },
|
||||
{
|
||||
value: '2:3',
|
||||
size: '512x768',
|
||||
text: '2:3(社交媒体,自拍)',
|
||||
icon: 'icon-aspect_2_3',
|
||||
},
|
||||
{
|
||||
value: '4:3',
|
||||
size: '1024x768',
|
||||
text: '4:3(文章配图,插画)',
|
||||
icon: 'icon-aspect_4_3',
|
||||
},
|
||||
{
|
||||
value: '9:16',
|
||||
size: '768x1366',
|
||||
text: '9:16(手机壁纸,人像)',
|
||||
icon: 'icon-aspect_9_16',
|
||||
},
|
||||
{
|
||||
value: '16:9',
|
||||
size: '1366x768',
|
||||
text: '16:9(桌面壁纸,风景)',
|
||||
icon: 'icon-aspect_16_9',
|
||||
},
|
||||
{ value: '3:2', size: '768x512', text: '3:2(1248x832)', icon: 'icon-aspect_3_2' },
|
||||
{ value: '3:4', size: '960x1280', text: '3:4(864x1152)', icon: 'icon-aspect_3_4' },
|
||||
{ value: '4:5', size: '960x1280', text: '4:5(896x1120)', icon: 'icon-aspect_4_5' },
|
||||
{
|
||||
value: '5:4',
|
||||
size: '1280x960',
|
||||
label: '5:4(1120x896)',
|
||||
icon: 'icon-aspect_5_4',
|
||||
},
|
||||
{ value: '21:9', size: '1344x576', text: '21:9(1536x658)', icon: 'icon-aspect_16_9' },
|
||||
])
|
||||
const params = ref({
|
||||
quality: qualities[0].value,
|
||||
size: sizes[0].value,
|
||||
style: styles[0].value,
|
||||
aspect_ratio: '1:1',
|
||||
size: '1024x1024',
|
||||
prompt: '',
|
||||
})
|
||||
const quality = ref(qualities[0].text)
|
||||
const size = ref(sizes[0].text)
|
||||
const style = ref(styles[0].text)
|
||||
const aspectRatio = ref(radioAspects.value[0].text)
|
||||
|
||||
const showQualityPicker = ref(false)
|
||||
const showStylePicker = ref(false)
|
||||
const showSizePicker = ref(false)
|
||||
const showModelPicker = ref(false)
|
||||
|
||||
@@ -313,6 +315,14 @@ onMounted(() => {
|
||||
.catch((e) => {
|
||||
showMessageError('获取模型列表失败:' + e.message)
|
||||
})
|
||||
|
||||
getMenus()
|
||||
.then((data) => {
|
||||
menus.value = data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取菜单失败:' + e.message })
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -408,10 +418,6 @@ const generate = () => {
|
||||
promptRef.value.focus()
|
||||
return showToast('请输入绘画提示词!')
|
||||
}
|
||||
|
||||
if (!params.value.seed) {
|
||||
params.value.seed = -1
|
||||
}
|
||||
params.value.session_id = getSessionId()
|
||||
httpPost('/api/dall/image', params.value)
|
||||
.then(() => {
|
||||
@@ -492,21 +498,10 @@ const imageView = (item) => {
|
||||
showImagePreview([item['img_url']])
|
||||
}
|
||||
|
||||
const qualityConfirm = (item) => {
|
||||
params.value.quality = item.selectedOptions[0].value
|
||||
quality.value = item.selectedOptions[0].text
|
||||
showQualityPicker.value = false
|
||||
}
|
||||
|
||||
const styleConfirm = (item) => {
|
||||
params.value.style = item.selectedOptions[0].value
|
||||
style.value = item.selectedOptions[0].text
|
||||
showStylePicker.value = false
|
||||
}
|
||||
|
||||
const sizeConfirm = (item) => {
|
||||
params.value.size = item.selectedOptions[0].value
|
||||
size.value = item.selectedOptions[0].text
|
||||
params.value.aspect_ratio = item.selectedOptions[0].value
|
||||
params.value.size = item.selectedOptions[0].size
|
||||
aspectRatio.value = item.selectedOptions[0].text
|
||||
showSizePicker.value = false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,338 +1,353 @@
|
||||
<template>
|
||||
<div class="mobile-mj">
|
||||
<van-form>
|
||||
<div class="text-line">图片比例</div>
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="4" v-for="item in rates" :key="item.value">
|
||||
<div
|
||||
:class="item.value === params.rate ? 'rate active' : 'rate'"
|
||||
@click="changeRate(item)"
|
||||
>
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
<div class="text-line">模型选择</div>
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="8" v-for="item in models" :key="item.value">
|
||||
<div
|
||||
:class="item.value === params.model ? 'model active' : 'model'"
|
||||
@click="changeModel(item)"
|
||||
>
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
<div class="text">
|
||||
<van-text-ellipsis :content="item.text" />
|
||||
</div>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="创意度">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.chaos"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.chaos)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="风格化">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.stylize"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.stylize)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="原始模式">
|
||||
<template #input>
|
||||
<van-switch v-model="params.raw" />
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-tabs v-model:active="activeName" @change="tabChange" animated>
|
||||
<van-tab title="文生图" name="txt2img">
|
||||
<div class="text-line">
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
maxlength="2000"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="图生图" name="img2img">
|
||||
<div class="text-line">
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
maxlength="2000"
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="垫图权重">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.iw"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
@update:model-value="showToast('当前值:' + params.iw)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="tip-text">
|
||||
提示:只有于 niji6 和 v6 模型支持一致性功能,如果选择其他模型此功能将会生成失败。
|
||||
</div>
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="params.cref"
|
||||
center
|
||||
clearable
|
||||
label="角色一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
<div v-if="menus['/mj']?.enabled">
|
||||
<van-form>
|
||||
<div class="text-line">图片比例</div>
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="4" v-for="item in rates" :key="item.value">
|
||||
<div
|
||||
:class="item.value === params.rate ? 'rate active' : 'rate'"
|
||||
@click="changeRate(item)"
|
||||
>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('cref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus" />
|
||||
</van-uploader>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="params.sref"
|
||||
center
|
||||
clearable
|
||||
label="风格一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
<div class="text">{{ item.text }}</div>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
<div class="text-line">模型选择</div>
|
||||
<div class="text-line">
|
||||
<van-row :gutter="10">
|
||||
<van-col :span="8" v-for="item in models" :key="item.value">
|
||||
<div
|
||||
:class="item.value === params.model ? 'model active' : 'model'"
|
||||
@click="changeModel(item)"
|
||||
>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('sref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus" />
|
||||
</van-uploader>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="一致性权重">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.cw"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.cw)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="融图" name="blend">
|
||||
<div class="tip-text">
|
||||
请上传两张以上的图片,最多不超过五张,超过五张图片请使用图生图功能。
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="换脸" name="swapFace">
|
||||
<div class="tip-text">请上传两张有脸部的图片,用左边图片的脸替换右边图片的脸。</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
</div>
|
||||
|
||||
<div class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm">
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ mjPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
<div class="icon">
|
||||
<van-image :src="item.img" fit="cover"></van-image>
|
||||
</div>
|
||||
<div class="text">
|
||||
<van-text-ellipsis :content="item.text" />
|
||||
</div>
|
||||
</div>
|
||||
</van-col>
|
||||
</van-row>
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="创意度">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.chaos"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.chaos)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="风格化">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.stylize"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.stylize)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<div class="text-line">
|
||||
<van-field label="原始模式">
|
||||
<template #input>
|
||||
<van-switch v-model="params.raw" />
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id" class="min-h-[270px]">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage(item)" size="small">删除</van-button>
|
||||
<div class="text-line">
|
||||
<van-tabs v-model:active="activeName" @change="tabChange" animated>
|
||||
<van-tab title="文生图" name="txt2img">
|
||||
<div class="text-line">
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
maxlength="2000"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['thumb_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
<template v-slot:error>
|
||||
<span style="margin-bottom: 20px">正在下载图片</span>
|
||||
<van-loading type="circular" color="#1989fa" size="40" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="opt" v-if="item['can_opt']">
|
||||
<van-grid :gutter="3" :column-num="4">
|
||||
<van-grid-item><a @click="upscale(1, item)" class="opt-btn">U1</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(2, item)" class="opt-btn">U2</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(3, item)" class="opt-btn">U3</a></van-grid-item>
|
||||
<van-grid-item><a @click="upscale(4, item)" class="opt-btn">U4</a></van-grid-item>
|
||||
<van-grid-item
|
||||
><a @click="variation(1, item)" class="opt-btn">V1</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(2, item)" class="opt-btn">V2</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(3, item)" class="opt-btn">V3</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(4, item)" class="opt-btn">V4</a></van-grid-item
|
||||
>
|
||||
</van-grid>
|
||||
</van-tab>
|
||||
<van-tab title="图生图" name="img2img">
|
||||
<div class="text-line">
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
rows="3"
|
||||
autosize
|
||||
maxlength="2000"
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(item)" circle />
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage(item, false)"
|
||||
circle
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-field label="垫图权重">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.iw"
|
||||
:max="1"
|
||||
:step="0.01"
|
||||
@update:model-value="showToast('当前值:' + params.iw)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div class="tip-text">
|
||||
提示:只有于 niji6 和 v6 模型支持一致性功能,如果选择其他模型此功能将会生成失败。
|
||||
</div>
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="params.cref"
|
||||
center
|
||||
clearable
|
||||
label="角色一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage(item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('cref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus" />
|
||||
</van-uploader>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<van-cell-group>
|
||||
<van-field
|
||||
v-model="params.sref"
|
||||
center
|
||||
clearable
|
||||
label="风格一致性"
|
||||
placeholder="请输入图片URL或者上传图片"
|
||||
>
|
||||
<template #button>
|
||||
<van-uploader @click="beforeUpload('sref')" :after-read="uploadImg">
|
||||
<van-button size="mini" type="primary" icon="plus" />
|
||||
</van-uploader>
|
||||
</template>
|
||||
</van-field>
|
||||
</van-cell-group>
|
||||
|
||||
<div class="text-line">
|
||||
<van-field label="一致性权重">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.cw"
|
||||
:max="100"
|
||||
:step="1"
|
||||
@update:model-value="showToast('当前值:' + params.cw)"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="融图" name="blend">
|
||||
<div class="tip-text">
|
||||
请上传两张以上的图片,最多不超过五张,超过五张图片请使用图生图功能。
|
||||
</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
</van-tab>
|
||||
<van-tab title="换脸" name="swapFace">
|
||||
<div class="tip-text">请上传两张有脸部的图片,用左边图片的脸替换右边图片的脸。</div>
|
||||
<div class="text-line">
|
||||
<van-uploader v-model="imgList" :after-read="uploadImg" />
|
||||
</div>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
|
||||
<div class="text-line">
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
</div>
|
||||
|
||||
<div class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm">
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ mjPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button style="display: none" class="copy-prompt" :data-clipboard-text="prompt" id="copy-btn">
|
||||
复制
|
||||
</button>
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id" class="min-h-[270px]">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage(item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['thumb_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
<template v-slot:error>
|
||||
<span style="margin-bottom: 20px">正在下载图片</span>
|
||||
<van-loading type="circular" color="#1989fa" size="40" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="opt" v-if="item['can_opt']">
|
||||
<van-grid :gutter="3" :column-num="4">
|
||||
<van-grid-item
|
||||
><a @click="upscale(1, item)" class="opt-btn">U1</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="upscale(2, item)" class="opt-btn">U2</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="upscale(3, item)" class="opt-btn">U3</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="upscale(4, item)" class="opt-btn">U4</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(1, item)" class="opt-btn">V1</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(2, item)" class="opt-btn">V2</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(3, item)" class="opt-btn">V3</a></van-grid-item
|
||||
>
|
||||
<van-grid-item
|
||||
><a @click="variation(4, item)" class="opt-btn">V4</a></van-grid-item
|
||||
>
|
||||
</van-grid>
|
||||
</div>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage(item)" circle />
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage(item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage(item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<button style="display: none" class="copy-prompt" :data-clipboard-text="prompt" id="copy-btn">
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FunDisabled />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo, getMenus } from '@/store/cache'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
@@ -340,6 +355,7 @@ import { showLoginDialog } from '@/utils/libs'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import Compressor from 'compressorjs'
|
||||
import FunDisabled from '@/components/ui/FunDisabled.vue'
|
||||
import {
|
||||
showConfirmDialog,
|
||||
showDialog,
|
||||
@@ -351,6 +367,7 @@ import {
|
||||
} from 'vant'
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const menus = ref({})
|
||||
|
||||
const activeColspan = ref([''])
|
||||
|
||||
@@ -437,6 +454,14 @@ onMounted(() => {
|
||||
.catch(() => {
|
||||
// router.push('/login')
|
||||
})
|
||||
|
||||
getMenus()
|
||||
.then((data) => {
|
||||
menus.value = data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取菜单失败:' + e.message })
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -1,265 +1,278 @@
|
||||
<template>
|
||||
<div class="mobile-sd">
|
||||
<van-form>
|
||||
<van-cell-group class="px-3 pt-3 pb-4">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="params.sampler"
|
||||
is-link
|
||||
readonly
|
||||
label="采样方法"
|
||||
placeholder="选择采样方法"
|
||||
@click="showSamplerPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showSamplerPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="samplers"
|
||||
@cancel="showSamplerPicker = false"
|
||||
@confirm="samplerConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field label="图片尺寸">
|
||||
<template #input>
|
||||
<van-row gutter="20">
|
||||
<van-col span="12">
|
||||
<el-input v-model="params.width" size="small" placeholder="宽" />
|
||||
</van-col>
|
||||
<van-col span="12">
|
||||
<el-input v-model="params.height" size="small" placeholder="高" />
|
||||
</van-col>
|
||||
</van-row>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field v-model.number="params.steps" label="迭代步数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="showInfo('值越大则代表细节越多,同时也意味着出图速度越慢,一般推荐20-30')"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model.number="params.cfg_scale" label="引导系数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="
|
||||
showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model.number="params.seed" label="随机因子" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="
|
||||
showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field label="高清修复">
|
||||
<template #input>
|
||||
<van-switch v-model="params.hd_fix" />
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<div v-if="params.hd_fix">
|
||||
<div v-if="menus['/sd']?.enabled">
|
||||
<van-form>
|
||||
<van-cell-group class="px-3 pt-3 pb-4">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="params.hd_scale_alg"
|
||||
v-model="params.sampler"
|
||||
is-link
|
||||
readonly
|
||||
label="放大算法"
|
||||
placeholder="选择放大算法"
|
||||
@click="showUpscalePicker = true"
|
||||
label="采样方法"
|
||||
placeholder="选择采样方法"
|
||||
@click="showSamplerPicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showUpscalePicker" position="bottom" teleport="#app">
|
||||
<van-popup v-model:show="showSamplerPicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="upscaleAlgArr"
|
||||
@cancel="showUpscalePicker = false"
|
||||
@confirm="upscaleConfirm"
|
||||
:columns="samplers"
|
||||
@cancel="showSamplerPicker = false"
|
||||
@confirm="samplerConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field v-model.number="params.hd_scale" label="放大倍数" />
|
||||
<van-field v-model.number="params.hd_steps" label="迭代步数" />
|
||||
|
||||
<van-field label="重绘幅度">
|
||||
<van-field label="图片尺寸">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.hd_redraw_rate"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
@update:model-value="showToast('当前值:' + params.hd_redraw_rate)"
|
||||
/>
|
||||
<van-row gutter="20">
|
||||
<van-col span="12">
|
||||
<el-input v-model="params.width" size="small" placeholder="宽" />
|
||||
</van-col>
|
||||
<van-col span="12">
|
||||
<el-input v-model="params.height" size="small" placeholder="高" />
|
||||
</van-col>
|
||||
</van-row>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field v-model.number="params.steps" label="迭代步数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')"
|
||||
@click="showInfo('值越大则代表细节越多,同时也意味着出图速度越慢,一般推荐20-30')"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
maxlength="2000"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
|
||||
<div class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm">
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ sdPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
<van-field v-model.number="params.cfg_scale" label="引导系数" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="
|
||||
showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
<van-field v-model.number="params.seed" label="随机因子" placeholder="">
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="
|
||||
showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<van-field label="高清修复">
|
||||
<template #input>
|
||||
<van-switch v-model="params.hd_fix" />
|
||||
</template>
|
||||
</van-field>
|
||||
|
||||
<div v-if="params.hd_fix">
|
||||
<div>
|
||||
<van-field
|
||||
v-model="params.hd_scale_alg"
|
||||
is-link
|
||||
readonly
|
||||
label="放大算法"
|
||||
placeholder="选择放大算法"
|
||||
@click="showUpscalePicker = true"
|
||||
/>
|
||||
<van-popup v-model:show="showUpscalePicker" position="bottom" teleport="#app">
|
||||
<van-picker
|
||||
:columns="upscaleAlgArr"
|
||||
@cancel="showUpscalePicker = false"
|
||||
@confirm="upscaleConfirm"
|
||||
/>
|
||||
</van-popup>
|
||||
</div>
|
||||
|
||||
<van-field v-model.number="params.hd_scale" label="放大倍数" />
|
||||
<van-field v-model.number="params.hd_steps" label="迭代步数" />
|
||||
|
||||
<van-field label="重绘幅度">
|
||||
<template #input>
|
||||
<van-slider
|
||||
v-model.number="params.hd_redraw_rate"
|
||||
:max="1"
|
||||
:step="0.1"
|
||||
@update:model-value="showToast('当前值:' + params.hd_redraw_rate)"
|
||||
/>
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<van-icon
|
||||
name="info-o"
|
||||
@click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')"
|
||||
/>
|
||||
</template>
|
||||
</van-field>
|
||||
</div>
|
||||
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
<van-field
|
||||
v-model="params.prompt"
|
||||
maxlength="2000"
|
||||
rows="3"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
|
||||
/>
|
||||
|
||||
<van-collapse v-model="activeColspan">
|
||||
<van-collapse-item title="反向提示词" name="neg_prompt">
|
||||
<van-field
|
||||
v-model="params.neg_prompt"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
autosize
|
||||
type="textarea"
|
||||
placeholder="不想出现在图片上的元素(例如:树,建筑)"
|
||||
/>
|
||||
</van-collapse-item>
|
||||
</van-collapse>
|
||||
|
||||
<div
|
||||
class="sticky bottom-4 bg-[var(--van-cell-group-background)] rounded-xl p-4 shadow-sm"
|
||||
>
|
||||
<button
|
||||
@click="generate"
|
||||
:disabled="loading"
|
||||
type="button"
|
||||
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2"
|
||||
>
|
||||
<i v-if="loading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-chuangzuo"></i>
|
||||
<span>{{ loading ? '创作中...' : '立即生成' }}({{ sdPower }}算力)</span>
|
||||
</button>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</div>
|
||||
</van-cell-group>
|
||||
</van-form>
|
||||
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
<h3 class="m-3">任务列表</h3>
|
||||
<div class="running-job-list pt-3 pb-3">
|
||||
<van-empty
|
||||
v-if="runningJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
<van-grid :gutter="10" :column-num="3" v-else>
|
||||
<van-grid-item v-for="item in runningJobs" :key="item.id">
|
||||
<div v-if="item.progress > 0">
|
||||
<van-image src="/images/img-holder.png"></van-image>
|
||||
<div class="progress">
|
||||
<van-circle
|
||||
v-model:current-rate="item.progress"
|
||||
:rate="item.progress"
|
||||
:speed="100"
|
||||
:text="item.progress + '%'"
|
||||
:stroke-width="60"
|
||||
size="90px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle />
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage($event, item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else class="task-in-queue">
|
||||
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
|
||||
<span class="text">排队中</span>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<h3 class="m-3">创作记录</h3>
|
||||
<div class="finish-job-list">
|
||||
<van-empty
|
||||
v-if="finishedJobs.length === 0"
|
||||
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
|
||||
image-size="80"
|
||||
description="暂无记录"
|
||||
/>
|
||||
|
||||
<van-list
|
||||
v-else
|
||||
v-model:error="error"
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
error-text="请求失败,点击重新加载"
|
||||
finished-text="没有更多了"
|
||||
@load="onLoad"
|
||||
>
|
||||
<van-grid :gutter="10" :column-num="2">
|
||||
<van-grid-item v-for="item in finishedJobs" :key="item.id">
|
||||
<div class="failed" v-if="item.progress === 101">
|
||||
<div class="title">任务失败</div>
|
||||
<div class="opt">
|
||||
<van-button size="small" @click="showErrMsg(item)">详情</van-button>
|
||||
<van-button type="danger" @click="removeImage($event, item)" size="small"
|
||||
>删除</van-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="job-item" v-else>
|
||||
<van-image
|
||||
:src="item['img_url']"
|
||||
:class="item['can_opt'] ? '' : 'upscale'"
|
||||
lazy-load
|
||||
@click="imageView(item)"
|
||||
fit="cover"
|
||||
>
|
||||
<template v-slot:loading>
|
||||
<van-loading type="spinner" size="20" />
|
||||
</template>
|
||||
</van-image>
|
||||
|
||||
<div class="remove">
|
||||
<el-button
|
||||
type="danger"
|
||||
:icon="Delete"
|
||||
@click="removeImage($event, item)"
|
||||
circle
|
||||
/>
|
||||
<el-button
|
||||
type="warning"
|
||||
v-if="item.publish"
|
||||
@click="publishImage($event, item, false)"
|
||||
circle
|
||||
>
|
||||
<i class="iconfont icon-cancel-share"></i>
|
||||
</el-button>
|
||||
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
|
||||
<i class="iconfont icon-share-bold"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showPrompt(item)" circle>
|
||||
<i class="iconfont icon-prompt"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-grid-item>
|
||||
</van-grid>
|
||||
</van-list>
|
||||
</div>
|
||||
<button
|
||||
style="display: none"
|
||||
class="copy-prompt-sd"
|
||||
:data-clipboard-text="prompt"
|
||||
id="copy-btn-sd"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FunDisabled />
|
||||
</div>
|
||||
<button
|
||||
style="display: none"
|
||||
class="copy-prompt-sd"
|
||||
:data-clipboard-text="prompt"
|
||||
id="copy-btn-sd"
|
||||
>
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { checkSession, getSystemInfo } from '@/store/cache'
|
||||
import { checkSession, getSystemInfo, getMenus } from '@/store/cache'
|
||||
import { getSessionId } from '@/store/session'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { showLoginDialog } from '@/utils/libs'
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import Clipboard from 'clipboard'
|
||||
import FunDisabled from '@/components/ui/FunDisabled.vue'
|
||||
import {
|
||||
showConfirmDialog,
|
||||
showDialog,
|
||||
@@ -276,6 +289,7 @@ const listBoxHeight = ref(window.innerHeight - 40)
|
||||
const mjBoxHeight = ref(window.innerHeight - 150)
|
||||
const isLogin = ref(false)
|
||||
const activeColspan = ref([''])
|
||||
const menus = ref({})
|
||||
|
||||
window.onresize = () => {
|
||||
listBoxHeight.value = window.innerHeight - 40
|
||||
@@ -352,6 +366,14 @@ onMounted(() => {
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取系统配置失败:' + e.message })
|
||||
})
|
||||
|
||||
getMenus()
|
||||
.then((data) => {
|
||||
menus.value = data
|
||||
})
|
||||
.catch((e) => {
|
||||
showNotify({ type: 'danger', message: '获取菜单失败:' + e.message })
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user