release: v4.2.8

整合开源版 v4.2.8 功能:Sora2 视频、路由重构、手机站开关、DALL-E 参考图,以及启动时自动同步数据表字段等修复与优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
RockYang
2026-08-03 11:12:37 +08:00
parent f8a01cb9a2
commit b18b8ccb02
91 changed files with 3326 additions and 3121 deletions
+4 -3
View File
@@ -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);
}
}
+3 -2
View File
@@ -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 {
+139
View File
@@ -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>
+1 -1
View File
@@ -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">
+25 -4
View File
@@ -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,
'&lt;/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);
}
}
}
}
+1 -1
View File
@@ -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>
+22 -20
View File
@@ -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;
+41 -19
View File
@@ -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',
+8 -1
View File
@@ -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,
+11 -2
View File
@@ -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" />
+52
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -64,7 +64,7 @@ export const JimengParams = {
type: 'image',
required: false,
placeholder: '请上传图片',
maxSize: 10,
maxSize: 15,
multiple: true,
maxCount: 10,
accept: '.png,.jpg,.jpeg',
+1
View File
@@ -71,6 +71,7 @@ export const useJimengStore = defineStore('jimeng', () => {
// 获取任务状态文本
const getTaskStatusText = (status) => {
const statusMap = {
submited: '任务已提交',
in_queue: '任务排队中',
generating: '任务执行中',
success: '任务成功',
+1 -1
View File
@@ -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
View File
@@ -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>
+7 -16
View File
@@ -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
View File
@@ -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:21248x832', icon: 'icon-aspect_3_2' },
'3:4': { value: '3:4', size: '960x1280', label: '3:4864x1152', icon: 'icon-aspect_3_4' },
'4:5': { value: '4:5', size: '960x1280', label: '4:5896x1120', icon: 'icon-aspect_4_5' },
'5:4': {
value: '5:4',
size: '1280x960',
label: '5:41120x896',
icon: 'icon-aspect_5_4',
},
'21:9': { value: '21:9', size: '1344x576', label: '21:91536x658', 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
View File
@@ -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
View File
@@ -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
-1
View File
@@ -229,7 +229,6 @@
>
<template #default="{ item }">
<div class="task-item">
<!-- 保持原有内容 -->
<div class="task-left">
<div class="task-preview">
<el-image
+5 -3
View File
@@ -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: {},
}
}
+4
View File
@@ -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>
+2 -4
View File
@@ -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)
+20 -2
View File
@@ -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>
+1 -1
View File
@@ -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"
+14 -5
View File
@@ -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: '',
+59 -44
View File
@@ -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()
-6
View File
@@ -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) => {
+16
View File
@@ -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>
+1 -1
View File
@@ -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 {
+15 -1
View File
@@ -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()
})
+15 -2
View File
@@ -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 })
})
})
// 支付处理
+225 -230
View File
@@ -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:21248x832', icon: 'icon-aspect_3_2' },
{ value: '3:4', size: '960x1280', text: '3:4864x1152', icon: 'icon-aspect_3_4' },
{ value: '4:5', size: '960x1280', text: '4:5896x1120', icon: 'icon-aspect_4_5' },
{
value: '5:4',
size: '1280x960',
label: '5:41120x896',
icon: 'icon-aspect_5_4',
},
{ value: '21:9', size: '1344x576', text: '21:91536x658', 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
}
+339 -314
View File
@@ -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(() => {
+241 -219
View File
@@ -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(() => {