ai_images/portal-ui/src/views/GeneratedImages.vue

370 lines
10 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="generated-images-page">
<div class="page-header">
<div class="panel-title">
<span>AI 文生图</span>
<span class="subtitle">使用 NanoBanana 最新模型生成高质量图像</span>
</div>
</div>
<div class="query-section">
<a-form :model="formState" layout="vertical">
<div class="form-grid">
<!-- 功能类型选择 -->
<div class="field">
<label>功能类型</label>
<a-select v-model:value="formState.functionType" style="width: 100%" @change="loadAiInfo">
<a-select-option value="1">标准文生图</a-select-option>
<a-select-option value="11">高级文生图</a-select-option>
</a-select>
</div>
<!-- NanoBanana 接口版本 -->
<div class="field">
<label>NanoBanana 接口</label>
<a-select v-model:value="formState.nanoApiType" style="width: 100%">
<a-select-option value="v1">V1官方 /generate</a-select-option>
<a-select-option value="v2">V2/generate-2</a-select-option>
<a-select-option value="pro">Pro/generate-pro</a-select-option>
</a-select>
</div>
<!-- 提示词 -->
<div class="field full-width">
<label>描述提示词 <span class="required">*</span></label>
<a-textarea
v-model:value="formState.text"
:rows="4"
placeholder="描述你想要生成的图像例如a happy dog running in the park, highly detailed, 8k"
/>
</div>
<!-- 宽高比 -->
<div class="field">
<label>宽高比</label>
<a-select v-model:value="formState.aspectRatio" style="width: 100%">
<a-select-option value="auto">自动 (auto)</a-select-option>
<a-select-option value="1:1">1:1 正方形</a-select-option>
<a-select-option value="16:9">16:9 横图</a-select-option>
<a-select-option value="9:16">9:16 竖图</a-select-option>
<a-select-option value="4:3">4:3</a-select-option>
<a-select-option value="3:2">3:2</a-select-option>
</a-select>
</div>
<!-- 分辨率Pro 必选 1K/2K/4K -->
<div class="field">
<label>分辨率 <span v-if="formState.nanoApiType === 'pro'" class="required">*</span></label>
<a-select v-model:value="formState.resolution" style="width: 100%">
<a-select-option value="1K">1K (标准)</a-select-option>
<a-select-option value="2K">2K (高清)</a-select-option>
<a-select-option value="4K">4K (超清)</a-select-option>
</a-select>
</div>
</div>
<div class="actions">
<a-button
type="primary"
size="large"
:loading="loading"
@click="generateImage"
style="min-width: 160px"
>
生成图像 (消耗 {{ price }} 余额)
</a-button>
<a-button @click="resetForm">重置</a-button>
</div>
</a-form>
</div>
<!-- 结果展示 -->
<div class="result-section" v-if="resultImage">
<div class="result-header">
<span class="result-title">生成结果</span>
<a-button type="link" @click="copyImageUrl">复制链接</a-button>
</div>
<div class="preview-content">
<a-image
:src="resultImage"
:preview="true"
class="result-image"
/>
</div>
<div class="task-info">
<p>任务ID: {{ taskId }}</p>
<p v-if="status">状态: {{ status === 1 ? '生成成功' : '处理中...' }}</p>
</div>
</div>
<!-- 历史记录 -->
<div class="history-section" v-if="historyList.length > 0">
<div class="panel-title">历史生成记录</div>
<a-table
:data-source="historyList"
:columns="columns"
:pagination="false"
row-key="id"
class="asset-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.result" width="80" :preview="true" />
</template>
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 1 ? 'green' : 'orange'">
{{ record.status === 1 ? '成功' : '生成中' }}
</a-tag>
</template>
</template>
</a-table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { nanoBananaGenerate, getAiManagerInfo } from '@/api/ai'
import { Message } from '@arco-design/web-vue'
const formState = reactive({
functionType: '1',
nanoApiType: 'v2',
text: '',
aspectRatio: 'auto',
resolution: '1K',
tags: ''
})
const loading = ref(false)
const price = ref(0)
const resultImage = ref('')
const taskId = ref('')
const status = ref(0)
const historyList = ref<any[]>([])
interface TableColumn {
title: string
dataIndex?: string
key?: string
width?: number
ellipsis?: boolean
}
const columns: TableColumn[] = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '提示词', dataIndex: 'text', ellipsis: true },
{ title: '图像', key: 'image', width: 100 },
{ title: '状态', key: 'status', width: 100 },
{ title: '时间', dataIndex: 'createTime' }
]
const loadAiInfo = async () => {
try {
const res = await getAiManagerInfo(formState.functionType)
if (res.price) price.value = res.price
} catch (e) {
console.error(e)
}
}
const generateImageFn = async () => {
if (!formState.text.trim()) {
Message.warning('请输入提示词')
return
}
if (formState.nanoApiType === 'pro' && !formState.resolution) {
Message.warning('Pro 接口必须选择分辨率1K / 2K / 4K')
return
}
loading.value = true
try {
const params = {
functionType: formState.functionType,
nanoApiType: formState.nanoApiType,
text: formState.text,
aspectRatio: formState.aspectRatio,
resolution: formState.resolution,
tags: formState.tags
}
const res = await nanoBananaGenerate(params)
if (res.code === 200) {
taskId.value = res.data || res.msg
Message.success('任务已提交任务ID: ' + taskId.value)
resultImage.value = '' // 清空等待回调或轮询
// 模拟轮询实际生产建议使用WebSocket或定时查询订单
pollResult(taskId.value)
} else {
Message.error(res.msg || '生成失败')
}
} catch (error: any) {
Message.error(error.message || '请求失败请检查网络或Token配置')
} finally {
loading.value = false
}
}
const pollResult = (tid: string) => {
let count = 0
const timer = setInterval(async () => {
count++
if (count > 30) { // 最多轮询30次
clearInterval(timer)
Message.warning('生成时间较长,请稍后在「我的作品」中查看')
return
}
// 这里实际应调用订单查询接口,暂时模拟成功
if (count > 8) {
clearInterval(timer)
resultImage.value = 'https://images.iqyjsnwv.com/sample-' + Date.now() + '.jpg' // 演示图片
status.value = 1
historyList.value.unshift({
id: Date.now(),
text: formState.text.substring(0, 30) + '...',
result: resultImage.value,
status: 1,
createTime: new Date().toLocaleString()
})
Message.success('图像生成成功!')
}
}, 1500)
}
const resetForm = () => {
formState.text = ''
resultImage.value = ''
taskId.value = ''
status.value = 0
}
const copyImageUrl = () => {
if (resultImage.value) {
navigator.clipboard.writeText(resultImage.value)
Message.success('链接已复制')
}
}
onMounted(() => {
loadAiInfo()
})
</script>
<style scoped lang="less">
.generated-images-page {
padding: 32px;
background: #f0f0f0;
min-height: 100vh;
color: #2c2c2c;
/* 继承全局粗野主义硬边阴影字体 */
.page-header {
margin-bottom: 32px;
border-bottom: 4px solid #2c2c2c;
padding-bottom: 16px;
.panel-title {
font-size: 32px;
font-weight: 900;
letter-spacing: -2px;
text-transform: uppercase;
color: #2c2c2c;
.subtitle {
font-size: 15px;
color: #ff0033;
margin-left: 16px;
font-weight: 900;
letter-spacing: -0.5px;
}
}
}
.query-section,
.result-section,
.history-section {
background: #f0f0f0;
border: 3px solid #2c2c2c;
padding: 28px;
margin-bottom: 32px;
box-shadow: 6px 6px 0 #2c2c2c;
/* 全局已覆盖圆角为0 */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
}
.full-width {
grid-column: 1 / -1;
}
label {
display: block;
margin-bottom: 12px;
font-weight: 900;
color: #2c2c2c;
text-transform: uppercase;
font-size: 14px;
letter-spacing: 0.5px;
}
.required {
color: #ff0033;
}
}
.actions {
margin-top: 24px;
display: flex;
gap: 16px;
}
.result-image {
max-width: 100%;
border: 4px solid #2c2c2c;
box-shadow: 8px 8px 0 #2c2c2c;
image-rendering: crisp-edges;
/* 高对比 by global */
}
.task-info {
margin-top: 20px;
padding: 16px;
background: #e8e8e8;
border: 2px solid #2c2c2c;
box-shadow: 4px 4px 0 #ff0033;
font-size: 14px;
color: #2c2c2c;
font-weight: 700;
}
.asset-table {
margin-top: 24px;
}
}
.brutalist-card {
border: 3px solid #2c2c2c;
box-shadow: 6px 6px 0 #2c2c2c;
}
/* 确保图片和按钮在视图中匹配 */
.result-image,
img {
border: 4px solid #2c2c2c !important;
box-shadow: 6px 6px 0 #2c2c2c !important;
}
@media (max-width: 768px) {
.generated-images-page {
padding: 20px;
}
.form-grid {
grid-template-columns: 1fr !important;
}
}
</style>