Files
geekai/web/src/views/mobile/pages/ImageDall.vue
2025-08-06 09:57:14 +08:00

523 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="mobile-sd">
<van-form @submit="generate">
<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"
/>
</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="text-line pt-6">
<el-tag>绘图消耗{{ dallPower }}算力当前算力{{ power }}</el-tag>
</div>
<div class="text-line">
<van-button round block type="primary" native-type="submit"> 立即生成 </van-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"
/>
</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>
</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>
</template>
<script setup>
import { checkSession, getSystemInfo } 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 {
showConfirmDialog,
showDialog,
showFailToast,
showImagePreview,
showNotify,
showSuccessToast,
showToast,
} from 'vant'
import { onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const listBoxHeight = ref(window.innerHeight - 40)
const mjBoxHeight = ref(window.innerHeight - 150)
const isLogin = ref(false)
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' },
]
const params = ref({
quality: qualities[0].value,
size: sizes[0].value,
style: styles[0].value,
prompt: '',
})
const quality = ref(qualities[0].text)
const size = ref(sizes[0].text)
const style = ref(styles[0].text)
const showQualityPicker = ref(false)
const showStylePicker = ref(false)
const showSizePicker = ref(false)
const showModelPicker = ref(false)
const runningJobs = ref([])
const finishedJobs = ref([])
const allowPulling = ref(true) // 是否允许轮询
const tastPullHandler = ref(null)
const router = useRouter()
const power = ref(0)
const dallPower = ref(0) // 画一张 DALL 图片消耗算力
const userId = ref(0)
const store = useSharedStore()
const clipboard = ref(null)
const prompt = ref('')
const models = ref([])
const selectedModel = ref(null)
onMounted(() => {
initData()
clipboard.value = new Clipboard('.copy-prompt-dall')
clipboard.value.on('success', () => {
showNotify({ type: 'success', message: '复制成功', duration: 1000 })
})
clipboard.value.on('error', () => {
showNotify({ type: 'danger', message: '复制失败', duration: 2000 })
})
getSystemInfo()
.then((res) => {
dallPower.value = res.data.dall_power
})
.catch((e) => {
showNotify({ type: 'danger', message: '获取系统配置失败:' + e.message })
})
// 获取模型列表
httpGet('/api/dall/models')
.then((res) => {
for (let i = 0; i < res.data.length; i++) {
models.value.push({
text: res.data[i].name,
value: res.data[i].id,
name: res.data[i].value,
})
}
selectedModel.value = models.value[0]?.text
params.value.model_id = models.value[0]?.value
})
.catch((e) => {
showMessageError('获取模型列表失败:' + e.message)
})
})
onUnmounted(() => {
clipboard.value.destroy()
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value)
}
})
const initData = () => {
checkSession()
.then((user) => {
power.value = user['power']
isLogin.value = true
fetchRunningJobs()
fetchFinishJobs(1)
tastPullHandler.value = setInterval(() => {
if (allowPulling.value) {
fetchRunningJobs()
}
}, 5000)
})
.catch(() => {
loading.value = false
})
}
const fetchRunningJobs = () => {
// 获取运行中的任务
httpGet(`/api/dall/jobs?finish=0`)
.then((res) => {
if (runningJobs.value.length !== res.data.items.length) {
fetchFinishJobs(1)
}
if (res.data.items.length === 0) {
allowPulling.value = false
}
runningJobs.value = res.data.items
})
.catch((e) => {
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
})
}
const loading = ref(false)
const finished = ref(false)
const error = ref(false)
const page = ref(0)
const pageSize = ref(10)
// 获取已完成的任务
const fetchFinishJobs = (page) => {
loading.value = true
httpGet(`/api/dall/jobs?finish=1&page=${page}&page_size=${pageSize.value}`)
.then((res) => {
const jobs = res.data.items
if (jobs.length < pageSize.value) {
finished.value = true
}
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?imageView2/1/w/480/h/600/q/75'
}
_jobs.push(jobs[i])
}
if (page === 1) {
finishedJobs.value = _jobs
} else {
finishedJobs.value = finishedJobs.value.concat(_jobs)
}
loading.value = false
})
.catch((e) => {
loading.value = false
showNotify({ type: 'danger', message: '获取任务失败:' + e.message })
})
}
const onLoad = () => {
page.value += 1
fetchFinishJobs(page.value)
}
// 创建绘图任务
const promptRef = ref(null)
const generate = () => {
if (!isLogin.value) {
return showLoginDialog(router)
}
if (params.value.prompt === '') {
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(() => {
showSuccessToast('绘画任务推送成功,请耐心等待任务执行...')
power.value -= dallPower.value
allowPulling.value = true
runningJobs.value.push({
progress: 0,
})
})
.catch((e) => {
showFailToast('任务推送失败:' + e.message)
})
}
const showPrompt = (item) => {
prompt.value = item.prompt
showConfirmDialog({
title: '绘画提示词',
message: item.prompt,
confirmButtonText: '复制',
cancelButtonText: '关闭',
})
.then(() => {
document.querySelector('#copy-btn-dall').click()
})
.catch(() => {})
}
const showErrMsg = (item) => {
showDialog({
title: '错误详情',
message: item['err_msg'],
}).then(() => {
// on close
})
}
const removeImage = (event, item) => {
event.stopPropagation()
showConfirmDialog({
title: '标题',
message: '此操作将会删除任务和图片,继续操作码?',
})
.then(() => {
httpGet('/api/dall/remove', { id: item.id, user_id: item.user_id })
.then(() => {
showSuccessToast('任务删除成功')
fetchFinishJobs(1)
})
.catch((e) => {
showFailToast('任务删除失败:' + e.message)
})
})
.catch(() => {
showToast('您取消了操作')
})
}
// 发布图片到作品墙
const publishImage = (event, item, action) => {
event.stopPropagation()
let text = '图片发布'
if (action === false) {
text = '取消发布'
}
httpGet('/api/dall/publish', { id: item.id, action: action, user_id: item.user_id })
.then(() => {
showSuccessToast(text + '成功')
item.publish = action
})
.catch((e) => {
showFailToast(text + '失败:' + e.message)
})
}
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
showSizePicker.value = false
}
const modelConfirm = (item) => {
params.value.model_id = item.selectedOptions[0].value
selectedModel.value = item.selectedOptions[0].text
showModelPicker.value = false
if (item.selectedOptions[0].name.startsWith('dall')) {
sizes = dalleSizes
} else {
sizes = fluxSizes
}
}
</script>
<style lang="scss">
@use '../../../assets/css/mobile/image-sd.scss' as *;
</style>