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

326 lines
8.6 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>
<!-- 提示词 -->
<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>
<!-- 分辨率 -->
<div class="field">
<label>分辨率</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 { generateImage, getAiManagerInfo } from '@/api/ai'
import { message } from 'ant-design-vue'
import type { ColumnType } from 'ant-design-vue/es/table'
const formState = reactive({
functionType: '1',
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[]>([])
const columns: ColumnType[] = [
{ 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
}
loading.value = true
try {
const params = {
functionType: formState.functionType,
text: formState.text,
aspectRatio: formState.aspectRatio,
resolution: formState.resolution,
tags: formState.tags
}
const res = await generateImage(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: 24px;
background: #1a1f2e;
min-height: 100vh;
color: #ddd;
.page-header {
margin-bottom: 24px;
.panel-title {
font-size: 24px;
font-weight: 600;
color: #fff;
.subtitle {
font-size: 14px;
color: #888;
margin-left: 12px;
font-weight: normal;
}
}
}
.query-section {
background: rgba(255,255,255,0.95);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.full-width {
grid-column: 1 / -1;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #1a1f2e;
}
.required {
color: #ff4d4f;
}
}
.actions {
margin-top: 24px;
display: flex;
gap: 12px;
}
.result-section, .history-section {
background: rgba(255,255,255,0.95);
border-radius: 12px;
padding: 24px;
margin-top: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.result-image {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
.task-info {
margin-top: 16px;
padding: 12px;
background: #f5f5f5;
border-radius: 6px;
font-size: 13px;
color: #555;
}
.asset-table {
margin-top: 16px;
}
}
@media (max-width: 768px) {
.generated-images-page {
padding: 16px;
}
.form-grid {
grid-template-columns: 1fr !important;
}
}
</style>