已初步完成
This commit is contained in:
parent
5a7dab7dc7
commit
d73a71d00f
|
|
@ -17,6 +17,9 @@ VITE_API_PAY_TARGET = 'https://sxwz.xueai.art' # http://43.248.133.202
|
|||
VITE_API_WORKFLOW_UPLOAD = 'https://designtools.xueai.art/workflow/file/upload' # https://sxwz.xueai.art/workflow https://designtools.xueai.art/workflow
|
||||
VITE_API_WORKFLOW_WS = 'wss://talkingdraw.xueai.art/testworkflow'
|
||||
|
||||
# 模型资源
|
||||
VITE_API_MODEL_RESOURCE = 'https://resources.xueai.art/AIGC'
|
||||
|
||||
# 是否开启KKFileView
|
||||
FILE_OPEN_PREVIEW = false
|
||||
# KKFileView服务器地址
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"nodeInfoList": {
|
||||
"prompt":{ "nodeId":"2", "fieldName":"prompt", "fieldValue":"" },
|
||||
"resolution":{ "nodeId":"2", "fieldName":"resolution", "fieldValue":"" },
|
||||
"proportion":{ "nodeId":"2", "fieldName":"aspect_ratio", "fieldValue":"" },
|
||||
"duration":{ "nodeId":"2", "fieldName":"duration", "fieldValue": 5},
|
||||
"audio":{ "nodeId":"2", "fieldName":"audio", "fieldValue": false}
|
||||
},
|
||||
"workflowId": "2036349280088231938",
|
||||
"display": {
|
||||
"promptPlaceholder": {"default": "描述你想生成的画面和动作。"},
|
||||
"prompt": {"default": ""},
|
||||
"resolution": {"default": "1k","options":[
|
||||
{ "value": "360", "label": "流畅 360P" },
|
||||
{ "value": "540", "label": "标清 540P" },
|
||||
{ "value": "720", "label": "高清 720P" },
|
||||
{ "value": "1k", "label": "超清 1K" }
|
||||
]},
|
||||
"proportion": {"default": "16:9","options":[
|
||||
{ "value": "21:9", "label": "21:9" },
|
||||
{ "value": "16:9", "label": "16:9" },
|
||||
{ "value": "4:3", "label": "4:3" },
|
||||
{ "value": "1:1", "label": "1:1" },
|
||||
{ "value": "3:4", "label": "3:4" },
|
||||
{ "value": "9:16", "label": "9:16" }
|
||||
]},
|
||||
"duration": {"default": 5,"options":[
|
||||
{ "value": 5, "label": "5秒" },
|
||||
{ "value": 10, "label": "10秒" },
|
||||
{ "value": 15, "label": "15秒" }
|
||||
]},
|
||||
"audio": {"default": false}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,7 @@ export {}
|
|||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
2: typeof import('./src/components/virtual-scroller/VirtualScroller copy 2.vue')['default']
|
||||
Canvas: typeof import('./src/components/canvas/index.vue')['default']
|
||||
copy: typeof import('./src/components/virtual-scroller/VirtualScroller copy.vue')['default']
|
||||
DialogBox: typeof import('./src/components/dialogBox/index.vue')['default']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
|
|
@ -26,15 +24,16 @@ declare module 'vue' {
|
|||
IEpStar: typeof import('~icons/ep/star')['default']
|
||||
ImageUploader: typeof import('./src/components/dialogBox/imageUploader/index.vue')['default']
|
||||
Img: typeof import('./src/components/Img/index.vue')['default']
|
||||
Model: typeof import('./src/components/dialogBox/model/index.vue')['default']
|
||||
Painting: typeof import('./src/components/dialogBox/model/painting.vue')['default']
|
||||
Pattern: typeof import('./src/components/dialogBox/pattern/index.vue')['default']
|
||||
Popover: typeof import('./src/components/Popover/index.vue')['default']
|
||||
Proportion: typeof import('./src/components/dialogBox/proportion/index.vue')['default']
|
||||
Quantity: typeof import('./src/components/dialogBox/quantity/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Select: typeof import('./src/components/Select/index.vue')['default']
|
||||
Time: typeof import('./src/components/dialogBox/Time/index.vue')['default']
|
||||
Video: typeof import('./src/components/dialogBox/model/video.vue')['default']
|
||||
VideoImageUploader: typeof import('./src/components/dialogBox/videoImageUploader/index.vue')['default']
|
||||
VirtualScroller: typeof import('./src/components/virtual-scroller/VirtualScroller.vue')['default']
|
||||
'VirtualScroller copy': typeof import('./src/components/virtual-scroller/VirtualScroller copy.vue')['default']
|
||||
'VirtualScroller copy 2': typeof import('./src/components/virtual-scroller/VirtualScroller copy 2.vue')['default']
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Cache-Control" content="public, max-age=3600" />
|
||||
<title>AI Painting</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -14,3 +14,8 @@ export function cancelOrCollect(query) {
|
|||
export function deleteGenerateHistory(query) {
|
||||
return service.delete('/taskRecordHistory/delete', { params: query })
|
||||
}
|
||||
|
||||
// 获取免费次数
|
||||
export function getFreeTimes(id) {
|
||||
return service.get('/plantformBalance/userBalances', { params: { id } })
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 2H15C15.5523 2 16 2.44772 16 3V5.5M16 12.5V15C16 15.5523 15.5523 16 15 16H12.5M5.5 16H3C2.44772 16 2 15.5523 2 15V12.5M2 5.5V3C2 2.44772 2.44772 2 3 2H5.5" stroke="#000F33" stroke-linecap="round"/>
|
||||
<path d="M8.53107 5.26725C8.69215 4.83194 9.30785 4.83194 9.46893 5.26725L10.0313 6.78706C10.2339 7.3345 10.6655 7.76612 11.2129 7.96869L12.7327 8.53107C13.1681 8.69215 13.1681 9.30785 12.7327 9.46893L11.2129 10.0313C10.6655 10.2339 10.2339 10.6655 10.0313 11.2129L9.46893 12.7327C9.30785 13.1681 8.69215 13.1681 8.53107 12.7327L7.96869 11.2129C7.76612 10.6655 7.3345 10.2339 6.78706 10.0313L5.26725 9.46893C4.83194 9.30785 4.83194 8.69215 5.26725 8.53107L6.78706 7.96869C7.3345 7.76612 7.76612 7.3345 7.96869 6.78706L8.53107 5.26725Z" fill="#000F33"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 869 B |
|
|
@ -1,4 +1,4 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 2H15C15.5523 2 16 2.44772 16 3V5.5M16 12.5V15C16 15.5523 15.5523 16 15 16H12.5M5.5 16H3C2.44772 16 2 15.5523 2 15V12.5M2 5.5V3C2 2.44772 2.44772 2 3 2H5.5" stroke="#666666" stroke-linecap="round"/>
|
||||
<path d="M10 10.1765C10.7712 10.1765 11.5127 10.456 12.0707 10.957C12.6288 11.458 12.9603 12.142 12.9967 12.867L13 13H5C5 12.2512 5.31607 11.533 5.87868 11.0035C6.44129 10.4739 7.20435 10.1765 8 10.1765H10ZM9 5C9.66304 5 10.2989 5.2479 10.7678 5.68916C11.2366 6.13042 11.5 6.7289 11.5 7.35294C11.5 7.97698 11.2366 8.57546 10.7678 9.01672C10.2989 9.45798 9.66304 9.70588 9 9.70588C8.33696 9.70588 7.70107 9.45798 7.23223 9.01672C6.76339 8.57546 6.5 7.97698 6.5 7.35294C6.5 6.7289 6.76339 6.13042 7.23223 5.68916C7.70107 5.2479 8.33696 5 9 5Z" fill="#666666"/>
|
||||
<path d="M12.5 2H15C15.5523 2 16 2.44772 16 3V5.5M16 12.5V15C16 15.5523 15.5523 16 15 16H12.5M5.5 16H3C2.44772 16 2 15.5523 2 15V12.5M2 5.5V3C2 2.44772 2.44772 2 3 2H5.5" stroke="#000F33" stroke-linecap="round"/>
|
||||
<path d="M8.53107 5.26725C8.69215 4.83194 9.30785 4.83194 9.46893 5.26725L10.0313 6.78706C10.2339 7.3345 10.6655 7.76612 11.2129 7.96869L12.7327 8.53107C13.1681 8.69215 13.1681 9.30785 12.7327 9.46893L11.2129 10.0313C10.6655 10.2339 10.2339 10.6655 10.0313 11.2129L9.46893 12.7327C9.30785 13.1681 8.69215 13.1681 8.53107 12.7327L7.96869 11.2129C7.76612 10.6655 7.3345 10.2339 6.78706 10.0313L5.26725 9.46893C4.83194 9.30785 4.83194 8.69215 5.26725 8.53107L6.78706 7.96869C7.3345 7.76612 7.76612 7.3345 7.96869 6.78706L8.53107 5.26725Z" fill="#000F33"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 875 B After Width: | Height: | Size: 869 B |
|
Before Width: | Height: | Size: 404 B After Width: | Height: | Size: 404 B |
|
|
@ -0,0 +1,4 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 2H15C15.5523 2 16 2.44772 16 3V5.5M16 12.5V15C16 15.5523 15.5523 16 15 16H12.5M5.5 16H3C2.44772 16 2 15.5523 2 15V12.5M2 5.5V3C2 2.44772 2.44772 2 3 2H5.5" stroke="#666666" stroke-linecap="round"/>
|
||||
<path d="M10 10.1765C10.7712 10.1765 11.5127 10.456 12.0707 10.957C12.6288 11.458 12.9603 12.142 12.9967 12.867L13 13H5C5 12.2512 5.31607 11.533 5.87868 11.0035C6.44129 10.4739 7.20435 10.1765 8 10.1765H10ZM9 5C9.66304 5 10.2989 5.2479 10.7678 5.68916C11.2366 6.13042 11.5 6.7289 11.5 7.35294C11.5 7.97698 11.2366 8.57546 10.7678 9.01672C10.2989 9.45798 9.66304 9.70588 9 9.70588C8.33696 9.70588 7.70107 9.45798 7.23223 9.01672C6.76339 8.57546 6.5 7.97698 6.5 7.35294C6.5 6.7289 6.76339 6.13042 7.23223 5.68916C7.70107 5.2479 8.33696 5 9 5Z" fill="#666666"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 875 B |
|
|
@ -29,8 +29,8 @@
|
|||
v-for="(option, index) in group.options"
|
||||
:key="option.value || index"
|
||||
class="dropdown-item"
|
||||
:class="{ selected: option.value === selectedValue }"
|
||||
@click.stop="selectOption(option)"
|
||||
:class="{ selected: option.value === selectedValue, disabled: option.disabled }"
|
||||
@click.stop="!option.disabled && selectOption(option)"
|
||||
>
|
||||
<slot name="option" :option="option" :selected="option.value === selectedValue">
|
||||
<div class="option-content">
|
||||
|
|
@ -46,8 +46,8 @@
|
|||
v-for="(option, index) in options"
|
||||
:key="option.value || index"
|
||||
class="dropdown-item"
|
||||
:class="{ selected: option.value === selectedValue }"
|
||||
@click.stop="selectOption(option)"
|
||||
:class="{ selected: option.value === selectedValue, disabled: option.disabled }"
|
||||
@click.stop="!option.disabled && selectOption(option)"
|
||||
>
|
||||
<slot name="option" :option="option" :selected="option.value === selectedValue">
|
||||
<div class="option-content">
|
||||
|
|
@ -321,12 +321,17 @@ onBeforeUnmount(() => {
|
|||
line-height: normal;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
.dropdown-item:hover:not(.disabled) {
|
||||
color: #333333;
|
||||
background-color: #f5f6f7;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.dropdown-item.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dropdown-item.selected {
|
||||
color: #333;
|
||||
font-weight: 400;
|
||||
|
|
|
|||
|
|
@ -89,7 +89,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<button class="confirm-btn" @click="handleSend">发送</button>
|
||||
<button class="confirm-btn" :class="{ loading: isSending }" :disabled="isSending" @click="handleSend">
|
||||
<span v-if="isSending">发送中...</span>
|
||||
<span v-else>发送</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -158,7 +161,7 @@
|
|||
|
||||
<script setup>
|
||||
import { generate } from '@/utils/websocket'
|
||||
import { useDisplayStore } from '@/stores'
|
||||
import { useDisplayStore, useUserStore } from '@/stores'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps({
|
||||
|
|
@ -213,6 +216,7 @@ const selectedReferenceImages = ref([])
|
|||
const currentEditingShapeIndex = ref(-1)
|
||||
const isPanelOpen = ref(false)
|
||||
const currentEditingContent = ref('')
|
||||
const isSending = ref(false)
|
||||
|
||||
const handleInput = (e) => {
|
||||
inputText.value = e.target.innerHTML
|
||||
|
|
@ -542,82 +546,93 @@ const handleClose = () => {
|
|||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
if (isSending.value) return
|
||||
if (!inputText.value) {
|
||||
ElMessage.error('请输入提示词')
|
||||
return
|
||||
}
|
||||
|
||||
const canvas = canvasRef.value
|
||||
const imageData = canvas.toDataURL('image/png')
|
||||
isSending.value = true
|
||||
|
||||
const imgs = []
|
||||
if (imageData) {
|
||||
imgs.push({ name: 'image_1', url: imageData })
|
||||
}
|
||||
try {
|
||||
const canvas = canvasRef.value
|
||||
const imageData = canvas.toDataURL('image/png')
|
||||
|
||||
allReferenceImages.value.forEach((img, index) => {
|
||||
imgs.push({ name: `image_${index + 2}`, url: img })
|
||||
})
|
||||
|
||||
const uploadImg = async (imgItem) => {
|
||||
if (!imgItem.url.startsWith('data:') && !imgItem.url.startsWith('blob:')) {
|
||||
return imgItem
|
||||
const imgs = []
|
||||
if (imageData) {
|
||||
imgs.push({ name: 'image_1', url: imageData })
|
||||
}
|
||||
|
||||
const response = await fetch(imgItem.url)
|
||||
const blob = await response.blob()
|
||||
const file = new File([blob], `${imgItem.name}.png`, { type: 'image/png' })
|
||||
allReferenceImages.value.forEach((img, index) => {
|
||||
imgs.push({ name: `image_${index + 2}`, url: img })
|
||||
})
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
const result = await request({
|
||||
url: import.meta.env.VITE_API_WORKFLOW_UPLOAD,
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
if (result.success || result.code === 0) {
|
||||
return { name: imgItem.name, url: result.url }
|
||||
const uploadImg = async (imgItem) => {
|
||||
if (!imgItem.url.startsWith('data:') && !imgItem.url.startsWith('blob:')) {
|
||||
return imgItem
|
||||
}
|
||||
|
||||
const response = await fetch(imgItem.url)
|
||||
const blob = await response.blob()
|
||||
const file = new File([blob], `${imgItem.name}.png`, { type: 'image/png' })
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
const result = await request({
|
||||
url: import.meta.env.VITE_API_WORKFLOW_UPLOAD,
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
if (result.success || result.code === 0) {
|
||||
return { name: imgItem.name, url: result.url }
|
||||
}
|
||||
return imgItem
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
return imgItem
|
||||
}
|
||||
return imgItem
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
return imgItem
|
||||
}
|
||||
|
||||
const uploadedImgs = await Promise.all(imgs.map(uploadImg))
|
||||
|
||||
const generateData = {
|
||||
model: 'banana',
|
||||
modelType: 'edit',
|
||||
prompt: inputText.value,
|
||||
proportion: '比例自动'
|
||||
}
|
||||
const data = {
|
||||
type: props.type,
|
||||
modelType: 'edit',
|
||||
AIGC: 'Painting',
|
||||
platform: 'runninghub',
|
||||
modelName: 'banana',
|
||||
quantity: 1,
|
||||
free: useUserStore().freeTimes,
|
||||
params: [
|
||||
{ name: 'prompt', data: inputText.value + '并且去除掉图1中的框' },
|
||||
{ name: 'index', data: 1 },
|
||||
],
|
||||
imgs: uploadedImgs,
|
||||
result: JSON.stringify(generateData)
|
||||
}
|
||||
|
||||
emit('send', {
|
||||
image: imageData,
|
||||
text: inputText.value,
|
||||
shapes: shapes.value
|
||||
})
|
||||
|
||||
await generate(data, generateData)
|
||||
handleClose()
|
||||
} finally {
|
||||
isSending.value = false
|
||||
}
|
||||
|
||||
const uploadedImgs = await Promise.all(imgs.map(uploadImg))
|
||||
|
||||
const generateData = {
|
||||
model: 'banana',
|
||||
modelType: 'edit',
|
||||
prompt: inputText.value,
|
||||
proportion: '比例自动'
|
||||
}
|
||||
const data = {
|
||||
AIGC: 'Painting',
|
||||
platform: 'runninghub',
|
||||
modelName: 'banana',
|
||||
params: [
|
||||
{ name: 'prompt', data: inputText.value + '并且去除掉图1中的框' },
|
||||
{ name: 'index', data: 1 },
|
||||
],
|
||||
imgs: uploadedImgs,
|
||||
result: JSON.stringify(generateData)
|
||||
}
|
||||
|
||||
emit('send', {
|
||||
image: imageData,
|
||||
text: inputText.value,
|
||||
shapes: shapes.value
|
||||
})
|
||||
|
||||
await generate('edit', data , generateData, props.type)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const removeReferenceImage = (index) => {
|
||||
|
|
@ -743,7 +758,7 @@ const handleBrushConfirm = () => {
|
|||
.preview-text {
|
||||
color: #333;
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
|
|
@ -911,6 +926,17 @@ const handleBrushConfirm = () => {
|
|||
font-weight: 400;
|
||||
line-height: normal;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:disabled {
|
||||
background: #999;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
background: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.brush-panel {
|
||||
|
|
@ -937,7 +963,7 @@ const handleBrushConfirm = () => {
|
|||
.brush-panel-title {
|
||||
color: #333;
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
|
|
@ -1003,7 +1029,7 @@ const handleBrushConfirm = () => {
|
|||
.brush-panel-text {
|
||||
color: #333;
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,17 @@ const props = defineProps({
|
|||
modelValue: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ value: 5, label: '5s' },
|
||||
{ value: 6, label: '6s' },
|
||||
{ value: 7, label: '7s' },
|
||||
{ value: 8, label: '8s' },
|
||||
{ value: 9, label: '9s' },
|
||||
{ value: 10, label: '10s' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -31,14 +42,7 @@ const quantity = computed({
|
|||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const quantityOptions = [
|
||||
{ value: 5, label: '5s' },
|
||||
{ value: 6, label: '6s' },
|
||||
{ value: 7, label: '7s' },
|
||||
{ value: 8, label: '8s' },
|
||||
{ value: 9, label: '9s' },
|
||||
{ value: 10, label: '10s' }
|
||||
]
|
||||
const quantityOptions = computed(() => props.options)
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
|||
|
|
@ -1,32 +1,56 @@
|
|||
<template>
|
||||
<Transition name="slide-up">
|
||||
<div class="input-container" :class="{ generate : !props.isGenerate }" @click="handleContainerClick">
|
||||
<div v-if="!props.isGenerate && props.type === 'painting'" class="title">AI绘画2026</div>
|
||||
<div v-if="!props.isGenerate && props.type === 'video'" class="title">AI视频2026</div>
|
||||
<div v-if="!props.isGenerate && props.type === 'Painting'" class="title">AI绘画2026</div>
|
||||
<div v-if="!props.isGenerate && props.type === 'Video'" class="title">AI视频2026</div>
|
||||
|
||||
<div class="sender-top">
|
||||
<div v-if="useDisplay.Sender_variant === 'default'" class="scroll-to-bottom-text" @click.stop="handleScrollToBottom">回到底部<img src="@/assets/dialog/ArrowDown.svg"></div>
|
||||
|
||||
<div v-show="modelType !== 'text'" class="upload-img-container">
|
||||
<div class="reference-diagram">
|
||||
<ImageUploader ref="referenceDiagramRef" v-model="referenceImages" :limit="4" @open-canvas="handleOpenCanvas" />
|
||||
<ImageUploader
|
||||
v-if="props.type === 'Painting'"
|
||||
ref="referenceDiagramRef"
|
||||
v-model="referenceImages"
|
||||
:limit="4"
|
||||
@open-canvas="handleOpenCanvas"
|
||||
/>
|
||||
<VideoImageUploader
|
||||
v-else-if="props.type === 'Video'"
|
||||
ref="referenceDiagramRef"
|
||||
v-model="referenceImages"
|
||||
:model-type="modelType"
|
||||
:images-count="modelDisplayConfig?.display?.images || 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Sender :key="useDisplay.Sender_variant" v-model="prompt" :variant="useDisplay.Sender_variant" :placeholder="promptPlaceholder" :submit-btn-disabled="isgerenate.value" :auto-size="autoSizeConfig">
|
||||
<template #prefix>
|
||||
<div v-show="useDisplay.Sender_variant !== 'default' && props.type === 'painting'" class="prefix-self-wrap">
|
||||
<Model v-model="model" v-model:typeValue="modelType" :type="props.type" />
|
||||
<Proportion v-model="proportion" v-model:resolution="resolution" />
|
||||
<div v-if="useDisplay.Sender_variant !== 'default' && props.type === 'Painting'" class="prefix-self-wrap">
|
||||
<paintingModel v-model="model" v-model:typeValue="modelType" />
|
||||
<paintingProportion
|
||||
v-model="proportion"
|
||||
v-model:resolution="resolution"
|
||||
:proportion-options="proportionOptions"
|
||||
:resolution-options="resolutionOptions"
|
||||
/>
|
||||
<Quantity v-model="quantity" />
|
||||
</div>
|
||||
|
||||
<div v-show="useDisplay.Sender_variant !== 'default' && props.type === 'video'" class="prefix-self-wrap">
|
||||
<Model v-model="model" v-model:typeValue="modelType" :type="props.type" />
|
||||
<div v-if="useDisplay.Sender_variant !== 'default' && props.type === 'Video'" class="prefix-self-wrap">
|
||||
<Pattern v-model="videoPattern" />
|
||||
<Proportion v-model="proportion" v-model:resolution="resolution" :type="props.type" />
|
||||
<Time v-model="time" />
|
||||
<videoModel v-model="model" v-model:typeValue="modelType" :video-pattern="videoPattern" />
|
||||
|
||||
<videoProportion
|
||||
v-model="proportion"
|
||||
v-model:resolution="resolution"
|
||||
:proportion-options="proportionOptions"
|
||||
:resolution-options="resolutionOptions"
|
||||
/>
|
||||
<Time v-model="duration" :options="durationOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -49,16 +73,20 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import Proportion from './proportion/index.vue'
|
||||
import paintingProportion from './proportion/painting.vue'
|
||||
import videoProportion from './proportion/video.vue'
|
||||
import paintingModel from './model/painting.vue'
|
||||
import videoModel from './model/video.vue'
|
||||
import Quantity from './quantity/index.vue'
|
||||
import Pattern from './pattern/index.vue'
|
||||
import Model from './model/index.vue'
|
||||
import ImageUploader from './imageUploader/index.vue'
|
||||
import VideoImageUploader from './videoImageUploader/index.vue'
|
||||
import Time from './Time/index.vue'
|
||||
import { Sender } from 'vue-element-plus-x'
|
||||
import { useDisplayStore } from '@/stores'
|
||||
import { useDisplayStore, useUserStore } from '@/stores'
|
||||
import { generate } from '@/utils/websocket'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { fetchModelConfig } from '@/utils/modelConfig'
|
||||
|
||||
const props = defineProps({
|
||||
isGenerate: {
|
||||
|
|
@ -71,30 +99,39 @@ const props = defineProps({
|
|||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'painting'
|
||||
default: 'Painting'
|
||||
}
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const useDisplay = useDisplayStore()
|
||||
const useUser = useUserStore()
|
||||
|
||||
const isgerenate = ref(false)
|
||||
|
||||
const model = ref('flux')
|
||||
const model = ref() // 模型
|
||||
const modelType = ref('text')
|
||||
|
||||
const prompt = ref('一个女孩在树下吃苹果')
|
||||
const promptPlaceholder = '描述你想生成的画面和动作。'
|
||||
const proportion = ref('16:9')
|
||||
const modelDisplayConfig = ref(null)
|
||||
const promptPlaceholder = ref('描述你想生成的画面和动作。') // 提示词占位符
|
||||
|
||||
const prompt = ref('') // 提示词
|
||||
const proportion = ref('16:9') // 比例
|
||||
const resolution = ref('1k') // 分辨率
|
||||
const referenceImages = ref([])
|
||||
|
||||
// 绘画
|
||||
const quantity = ref(1)
|
||||
const resolution = ref('1k')
|
||||
const quantity = ref(1) // 生成数量
|
||||
|
||||
// 视频
|
||||
const time = ref(5)
|
||||
const videoPattern = ref('全能参考')
|
||||
const duration = ref(5) // 时间
|
||||
const videoPattern = ref('文生视频') // 视频模式下,默认值为'文生视频'
|
||||
|
||||
const resolutionOptions = ref([])
|
||||
const proportionOptions = ref([])
|
||||
const durationOptions = ref([])
|
||||
|
||||
const isInitialized = ref(false)
|
||||
|
||||
const autoSizeConfig = computed(() => {
|
||||
if (useDisplay.Sender_variant !== 'default') {
|
||||
|
|
@ -104,10 +141,56 @@ const autoSizeConfig = computed(() => {
|
|||
}
|
||||
})
|
||||
|
||||
const loadModelConfig = async (modelName, currentModelType) => {
|
||||
try {
|
||||
const config = await fetchModelConfig(props.type, modelName, currentModelType)
|
||||
modelDisplayConfig.value = config
|
||||
|
||||
if (config.display) {
|
||||
const display = config.display
|
||||
|
||||
if (display.promptPlaceholder) {
|
||||
promptPlaceholder.value = display.promptPlaceholder.default || '描述你想生成的画面和动作。'
|
||||
}
|
||||
|
||||
if (display.prompt && !isInitialized.value) {
|
||||
prompt.value = display.prompt.default || ''
|
||||
}
|
||||
|
||||
if (display.resolution) {
|
||||
resolution.value = display.resolution.default || '1k'
|
||||
resolutionOptions.value = display.resolution.options || []
|
||||
}
|
||||
|
||||
if (display.proportion) {
|
||||
proportion.value = display.proportion.default || '16:9'
|
||||
proportionOptions.value = display.proportion.options || []
|
||||
}
|
||||
|
||||
if (display.duration) {
|
||||
duration.value = display.duration.default || 5
|
||||
durationOptions.value = display.duration.options || []
|
||||
}
|
||||
}
|
||||
|
||||
isInitialized.value = true
|
||||
|
||||
return config
|
||||
} catch (error) {
|
||||
console.error('加载模型配置失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const handleStart = async () => {
|
||||
const currentType = props.type
|
||||
let currentModelType = modelType.value
|
||||
|
||||
if(model.value === 'Seedance 2.0') {
|
||||
ElMessage.primary('敬请期待 Seedance 2.0')
|
||||
return
|
||||
}
|
||||
|
||||
if (!props.isGenerate) {
|
||||
router.push({ name: 'home', query: { loading: false, Generate: true, type: currentType } })
|
||||
}
|
||||
|
|
@ -116,21 +199,17 @@ const handleStart = async () => {
|
|||
ElMessage.error('请输入提示词')
|
||||
return
|
||||
}
|
||||
if (modelType.value === 'image' && !referenceImages.value.length){
|
||||
ElMessage.warning('请上传图片')
|
||||
return
|
||||
}
|
||||
|
||||
isgerenate.value = true
|
||||
console.log('生成开始', isgerenate.value)
|
||||
const imgs = []
|
||||
referenceImages.value.forEach((img, index) => {
|
||||
imgs.push({ name: `image_${index + 1}`, url: img.url })
|
||||
})
|
||||
// console.log('imgs', imgs)
|
||||
// 判断视频模式下是否文生视频
|
||||
if (currentType === 'video') {
|
||||
if (imgs.length > 0) {
|
||||
currentModelType = 'image'
|
||||
} else {
|
||||
currentModelType = 'text'
|
||||
}
|
||||
}
|
||||
|
||||
const generateData = {
|
||||
model: model.value,
|
||||
|
|
@ -140,24 +219,29 @@ const handleStart = async () => {
|
|||
referenceImages: referenceImages.value,
|
||||
quantity: quantity.value,
|
||||
resolution: resolution.value,
|
||||
time: time.value,
|
||||
duration: duration.value,
|
||||
videoPattern: videoPattern.value
|
||||
}
|
||||
|
||||
const data = {
|
||||
AIGC: 'Painting',
|
||||
type: currentType,
|
||||
modelType: currentModelType,
|
||||
AIGC: currentType,
|
||||
platform: 'runninghub',
|
||||
modelName: model.value,
|
||||
quantity: quantity.value,
|
||||
free: useUser.freeTimes,
|
||||
params: [
|
||||
{ name: 'prompt', data: prompt.value},
|
||||
{ name: 'quantity', data: quantity.value},
|
||||
{ name: 'aspect_ratio', data: proportion.value},
|
||||
{ name: 'proportion', data: proportion.value},
|
||||
{ name: 'resolution', data: resolution.value},
|
||||
{ name: 'duration', data: duration.value}
|
||||
],
|
||||
imgs,
|
||||
result: JSON.stringify(generateData)
|
||||
}
|
||||
await generate(currentModelType, data, generateData, currentType)
|
||||
await generate(data, generateData)
|
||||
console.log('生成中', isgerenate.value)
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +255,7 @@ const fillParamsFromResult = (resultData) => {
|
|||
if (resultData.referenceImages !== undefined) referenceImages.value = resultData.referenceImages
|
||||
if (resultData.quantity !== undefined) quantity.value = resultData.quantity
|
||||
if (resultData.resolution !== undefined) resolution.value = resultData.resolution
|
||||
if (resultData.time !== undefined) time.value = resultData.time
|
||||
if (resultData.duration !== undefined) duration.value = resultData.duration
|
||||
if (resultData.videoPattern !== undefined) videoPattern.value = resultData.videoPattern
|
||||
}
|
||||
|
||||
|
|
@ -198,25 +282,30 @@ const handleOpenCanvas = (data) => {
|
|||
watch(() => useDisplay.isSubGerenate, (newValue) => {
|
||||
console.log('生成状态', newValue)
|
||||
isgerenate.value = newValue
|
||||
// handleScrollToBottom()
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => modelType.value, (newValue) => {
|
||||
console.log('modelType.value', newValue)
|
||||
watch([() => model.value, () => modelType.value], async ([newModel, newModelType]) => {
|
||||
console.log('模型或类型改变:', newModel, newModelType)
|
||||
if (newModel && newModelType) {
|
||||
await loadModelConfig(newModel, newModelType)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.type, (newType) => {
|
||||
if (newType === 'video') {
|
||||
model.value = 'Vidu Q3-T2V'
|
||||
if (newType === 'Video') {
|
||||
model.value = 'LTX2.0'
|
||||
} else {
|
||||
model.value = 'flux'
|
||||
}
|
||||
})
|
||||
const chargeType = newType === 'Painting' ? 1 : 4
|
||||
useUser.fetchFreeTimes(chargeType)
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
if(props.type === 'video'){
|
||||
model.value = 'Vidu Q3-T2V'
|
||||
}
|
||||
// 组件挂载时获取免费次数
|
||||
onMounted(async () => {
|
||||
const chargeType = props.type === 'Painting' ? 1 : 4
|
||||
await useUser.fetchFreeTimes(chargeType)
|
||||
console.log('免费次数', useUser.freeTimes)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@
|
|||
|
||||
<script setup>
|
||||
import Select from '@/components/Select/index.vue'
|
||||
import paintingConfig from '@/config/modelConfig/painting.json'
|
||||
import videoConfig from '@/config/modelConfig/video.json'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
|
@ -24,15 +22,52 @@ const props = defineProps({
|
|||
typeValue: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'painting'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:typeValue'])
|
||||
|
||||
const paintingConfig = ref({
|
||||
generate: [],
|
||||
edit: [],
|
||||
vision: []
|
||||
})
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const url = `${import.meta.env.VITE_API_MODEL_RESOURCE}/static/public/Platform/AIGC_modelConfig/painting.json`
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
paintingConfig.value = data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch painting config:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fetchConfig()
|
||||
|
||||
watch(() => paintingConfig.value, (newConfig) => {
|
||||
const allModels = [
|
||||
...(newConfig.generate || []),
|
||||
...(newConfig.edit || []),
|
||||
...(newConfig.vision || [])
|
||||
]
|
||||
if (allModels.length > 0) {
|
||||
const enabledModels = allModels.filter(m => !m.disabled)
|
||||
if (enabledModels.length > 0) {
|
||||
const currentModelExists = enabledModels.find(m => m.value === props.modelValue)
|
||||
if (!currentModelExists) {
|
||||
const firstEnabled = enabledModels[0].value
|
||||
emit('update:modelValue', firstEnabled)
|
||||
const newType = getModelType(firstEnabled)
|
||||
emit('update:typeValue', newType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
|
||||
const model = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
|
|
@ -42,35 +77,11 @@ const model = computed({
|
|||
}
|
||||
})
|
||||
|
||||
const getModelsByType = (type) => {
|
||||
if (type === 'video') {
|
||||
return videoConfig.video || []
|
||||
}
|
||||
const config = paintingConfig
|
||||
return {
|
||||
generate: config.generate || [],
|
||||
edit: config.edit || [],
|
||||
vision: config.vision || []
|
||||
}
|
||||
}
|
||||
|
||||
const modelData = computed(() => {
|
||||
return getModelsByType(props.type)
|
||||
})
|
||||
|
||||
const isVideo = computed(() => props.type === 'video')
|
||||
|
||||
const generateModels = computed(() => modelData.value.generate || [])
|
||||
const editModels = computed(() => modelData.value.edit || [])
|
||||
const visionModels = computed(() => modelData.value.vision || [])
|
||||
const generateModels = computed(() => paintingConfig.value.generate || [])
|
||||
const editModels = computed(() => paintingConfig.value.edit || [])
|
||||
const visionModels = computed(() => paintingConfig.value.vision || [])
|
||||
|
||||
const modelGroups = computed(() => {
|
||||
if (isVideo.value) {
|
||||
return [{
|
||||
label: '选择模型',
|
||||
options: modelData.value
|
||||
}]
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: '生成模型',
|
||||
|
|
@ -88,9 +99,6 @@ const modelGroups = computed(() => {
|
|||
})
|
||||
|
||||
const getModelType = (value) => {
|
||||
if (isVideo.value) {
|
||||
return 'text'
|
||||
}
|
||||
if (generateModels.value.find(m => m.value === value)) {
|
||||
return 'text'
|
||||
}
|
||||
|
|
@ -102,6 +110,25 @@ const getModelType = (value) => {
|
|||
}
|
||||
return 'text'
|
||||
}
|
||||
|
||||
const getFirstEnabledModel = () => {
|
||||
const allModels = [...generateModels.value, ...editModels.value, ...visionModels.value]
|
||||
const firstEnabled = allModels.find(m => !m.disabled)
|
||||
return firstEnabled ? firstEnabled.value : ''
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
const allModels = [...generateModels.value, ...editModels.value, ...visionModels.value]
|
||||
const currentModel = allModels.find(m => m.value === newValue)
|
||||
if (currentModel && currentModel.disabled) {
|
||||
const firstEnabled = getFirstEnabledModel()
|
||||
if (firstEnabled) {
|
||||
emit('update:modelValue', firstEnabled)
|
||||
const newType = getModelType(firstEnabled)
|
||||
emit('update:typeValue', newType)
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
<template>
|
||||
<Select
|
||||
v-model="model"
|
||||
:options="modelGroups"
|
||||
class="model-select"
|
||||
position="top"
|
||||
>
|
||||
<template #prefix>
|
||||
<img src="@/assets/dialog/model.svg" alt="" style="width: 16px;">
|
||||
</template>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Select from '@/components/Select/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
typeValue: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
videoPattern: {
|
||||
type: String,
|
||||
default: '文生视频'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:typeValue'])
|
||||
|
||||
const videoConfig = ref({})
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const url = `${import.meta.env.VITE_API_MODEL_RESOURCE}/static/public/Platform/AIGC_modelConfig/video.json`
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
videoConfig.value = data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch video config:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fetchConfig()
|
||||
|
||||
watch(() => videoConfig.value, (newConfig) => {
|
||||
const models = newConfig[props.videoPattern] || []
|
||||
if (models.length > 0) {
|
||||
const enabledModels = models.filter(m => !m.disabled)
|
||||
if (enabledModels.length > 0) {
|
||||
const currentModelExists = enabledModels.find(m => m.value === props.modelValue)
|
||||
if (!currentModelExists) {
|
||||
model.value = enabledModels[0].value
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
|
||||
const model = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
const newType = getModelType(props.videoPattern)
|
||||
emit('update:typeValue', newType)
|
||||
}
|
||||
})
|
||||
|
||||
const modelGroups = computed(() => {
|
||||
const models = videoConfig.value[props.videoPattern] || []
|
||||
return models
|
||||
})
|
||||
|
||||
const getModelType = (value) => {
|
||||
switch (value) {
|
||||
case '文生视频':
|
||||
return 'text'
|
||||
case '首尾帧':
|
||||
return 'image'
|
||||
case '数字人':
|
||||
return 'digitalHuman'
|
||||
default:
|
||||
return 'text'
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.videoPattern, (newPattern) => {
|
||||
const models = videoConfig.value[newPattern] || []
|
||||
if (models.length > 0) {
|
||||
const enabledModels = models.filter(m => !m.disabled)
|
||||
if (enabledModels.length > 0) {
|
||||
const currentModelExists = enabledModels.find(m => m.value === props.modelValue)
|
||||
if (!currentModelExists) {
|
||||
model.value = enabledModels[0].value
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
const models = videoConfig.value[props.videoPattern] || []
|
||||
const currentModel = models.find(m => m.value === newValue)
|
||||
if (currentModel && currentModel.disabled) {
|
||||
const enabledModels = models.filter(m => !m.disabled)
|
||||
if (enabledModels.length > 0) {
|
||||
model.value = enabledModels[0].value
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.model-select {
|
||||
:deep(.select-header) {
|
||||
height: 40px;
|
||||
padding: 0 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #E8E9EB;
|
||||
background: #f5f6f7;
|
||||
|
||||
&:hover {
|
||||
background: #e9eaeb;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.select-text) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.dropdown-menu) {
|
||||
max-height: 510px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.dropdown-item) {
|
||||
min-width: 120px;
|
||||
|
||||
&.active {
|
||||
background: rgba(0, 15, 51, 0.10);
|
||||
color: #000F33;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -19,10 +19,11 @@
|
|||
|
||||
<script setup>
|
||||
import Select from '@/components/Select/index.vue'
|
||||
import videoPattern1 from '@/assets/dialog/videoPattern1.svg'
|
||||
|
||||
import videoPattern2 from '@/assets/dialog/videoPattern2.svg'
|
||||
import videoPattern3 from '@/assets/dialog/videoPattern3.svg'
|
||||
import videoPattern4 from '@/assets/dialog/videoPattern4.svg'
|
||||
import videoPattern5 from '@/assets/dialog/videoPattern5.svg'
|
||||
import videoPattern6 from '@/assets/dialog/videoPattern6.svg'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
|
@ -39,10 +40,12 @@ const quantity = computed({
|
|||
})
|
||||
|
||||
const quantityOptions = [
|
||||
{ value: '全能参考', label: '全能参考', labelText: '全能参考', icon: videoPattern1 },
|
||||
{ value: '文生视频', label: '文生视频', labelText: '文生视频', icon: videoPattern2 },
|
||||
{ value: '首尾帧', label: '首尾帧', labelText: '首尾帧', icon: videoPattern2 },
|
||||
{ value: '智能多帧', label: '智能多帧', labelText: '智能多帧', icon: videoPattern3 },
|
||||
{ value: '主体参考', label: '主体参考', labelText: '主体参考', icon: videoPattern4 }
|
||||
{ value: '数字人', label: '数字人', labelText: '数字人', icon: videoPattern2 },
|
||||
{ value: '全能参考', label: '全能参考', labelText: '全能参考', icon: videoPattern4 },
|
||||
{ value: '智能多帧', label: '智能多帧', labelText: '智能多帧', icon: videoPattern5 },
|
||||
{ value: '主体参考', label: '主体参考', labelText: '主体参考', icon: videoPattern6 }
|
||||
]
|
||||
|
||||
const selectedIcon = computed(() => {
|
||||
|
|
|
|||
|
|
@ -2,37 +2,37 @@
|
|||
<Popover placement="top" :width="400">
|
||||
<div class="proportion-container">
|
||||
<div class="section">
|
||||
<h3>选择比例</h3>
|
||||
<div class="proportion-options" :style="{ marginBottom: props.type === 'video' ? '0px' : '20px' }">
|
||||
<div
|
||||
v-for="item in proportionOptions"
|
||||
:key="item.value"
|
||||
class="proportion-item"
|
||||
:class="{ active: proportion === item.value }"
|
||||
:style="getProportionStyle(item.value)"
|
||||
@click="selectProportion(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<h3>选择比例</h3>
|
||||
<div class="proportion-options">
|
||||
<div
|
||||
v-for="item in proportionOptions"
|
||||
:key="item.value"
|
||||
class="proportion-item"
|
||||
:class="{ active: proportion === item.value }"
|
||||
:style="getProportionStyle(item.value)"
|
||||
@click="selectProportion(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="type === 'painting'" class="section">
|
||||
<div v-if="resolutionOptions.length > 0" class="section">
|
||||
<h3>选择分辨率</h3>
|
||||
<div class="resolution-options">
|
||||
<div
|
||||
v-for="item in resolutionOptions"
|
||||
:key="item.value"
|
||||
class="resolution-item"
|
||||
:class="{ active: resolution === item.value }"
|
||||
@click="selectResolution(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div
|
||||
v-for="item in resolutionOptions"
|
||||
:key="item.value"
|
||||
class="resolution-item"
|
||||
:class="{ active: resolution === item.value }"
|
||||
@click="selectResolution(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="type === 'painting'" class="section">
|
||||
<div class="section">
|
||||
<h3>尺寸(px)</h3>
|
||||
<div class="size-inputs">
|
||||
<div class="input-group">
|
||||
|
|
@ -60,7 +60,6 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import Popover from '@/components/Popover/index.vue'
|
||||
import lockIcon from '@/assets/dialog/lock.svg'
|
||||
import lockNoIcon from '@/assets/dialog/lockNo.svg'
|
||||
|
|
@ -74,9 +73,25 @@ const props = defineProps({
|
|||
type: String,
|
||||
default: '2k'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'painting'
|
||||
proportionOptions: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ value: '智能', label: '智能' },
|
||||
{ value: '21:9', label: '21:9' },
|
||||
{ value: '16:9', label: '16:9' },
|
||||
{ value: '4:3', label: '4:3' },
|
||||
{ value: '1:1', label: '1:1' },
|
||||
{ value: '3:4', label: '3:4' },
|
||||
{ value: '9:16', label: '9:16' }
|
||||
]
|
||||
},
|
||||
resolutionOptions: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ value: '1k', label: '标清 1K' },
|
||||
{ value: '2k', label: '高清 2K' },
|
||||
{ value: '4k', label: '超清 4K' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -92,24 +107,6 @@ const resolution = computed({
|
|||
set: (value) => emit('update:resolution', value)
|
||||
})
|
||||
|
||||
const proportionOptions = [
|
||||
{ value: '智能', label: '智能' },
|
||||
{ value: '21:9', label: '21:9' },
|
||||
{ value: '16:9', label: '16:9' },
|
||||
// { value: '3:2', label: '3:2' },
|
||||
{ value: '4:3', label: '4:3' },
|
||||
{ value: '1:1', label: '1:1' },
|
||||
{ value: '3:4', label: '3:4' },
|
||||
// { value: '2:3', label: '2:3' },
|
||||
{ value: '9:16', label: '9:16' }
|
||||
]
|
||||
|
||||
const resolutionOptions = [
|
||||
{ value: '1k', label: '标清 1K' },
|
||||
{ value: '2k', label: '高清 2K' },
|
||||
{ value: '4k', label: '超清 4K' }
|
||||
]
|
||||
|
||||
const width = ref(2048)
|
||||
const height = ref(2048)
|
||||
const isLocked = ref(true)
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
<template>
|
||||
<Popover placement="top" :width="400">
|
||||
<div class="proportion-container">
|
||||
<div class="section">
|
||||
<h3>选择比例</h3>
|
||||
<div class="proportion-options" :style="{ marginBottom: props.type === 'Video' ? '0px' : '20px' }">
|
||||
<div
|
||||
v-for="item in proportionOptions"
|
||||
:key="item.value"
|
||||
class="proportion-item"
|
||||
:class="{ active: proportion === item.value }"
|
||||
:style="getProportionStyle(item.value)"
|
||||
@click="selectProportion(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>选择分辨率</h3>
|
||||
<div class="resolution-options">
|
||||
<div
|
||||
v-for="item in resolutionOptions"
|
||||
:key="item.value"
|
||||
class="resolution-item"
|
||||
:class="{ active: resolution === item.value }"
|
||||
@click="selectResolution(item.value)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
<div class="choice-btn">
|
||||
<img src="@/assets/dialog/proportion.svg" alt="" style="width: 16px;">
|
||||
<span>{{ proportion }}</span>
|
||||
<div style="border: 0.5px solid #000; width: 1px; height:13px;margin: 0 5px;" />
|
||||
<span>{{ resolution }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Popover from '@/components/Popover/index.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '1:1'
|
||||
},
|
||||
resolution: {
|
||||
type: String,
|
||||
default: '2k'
|
||||
},
|
||||
proportionOptions: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ value: '21:9', label: '21:9' },
|
||||
{ value: '16:9', label: '16:9' },
|
||||
{ value: '4:3', label: '4:3' },
|
||||
{ value: '1:1', label: '1:1' },
|
||||
{ value: '3:4', label: '3:4' },
|
||||
{ value: '9:16', label: '9:16' }
|
||||
]
|
||||
},
|
||||
resolutionOptions: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ value: '360', label: '流畅 360P' },
|
||||
{ value: '540', label: '标清 540P' },
|
||||
{ value: '720', label: '高清 720P' },
|
||||
{ value: '1k', label: '超清 1K' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:resolution'])
|
||||
|
||||
const proportion = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const resolution = computed({
|
||||
get: () => props.resolution,
|
||||
set: (value) => emit('update:resolution', value)
|
||||
})
|
||||
|
||||
const selectProportion = (value) => {
|
||||
proportion.value = value
|
||||
}
|
||||
|
||||
const selectResolution = (value) => {
|
||||
resolution.value = value
|
||||
}
|
||||
|
||||
const getProportionStyle = (value) => {
|
||||
if (value === '智能') {
|
||||
return {
|
||||
'--width': '20px',
|
||||
'--height': '20px'
|
||||
}
|
||||
}
|
||||
const [w, h] = value.split(':').map(Number)
|
||||
const aspectRatio = w / h
|
||||
const baseSize = 20
|
||||
|
||||
if (aspectRatio > 1) {
|
||||
return {
|
||||
'--width': `${baseSize}px`,
|
||||
'--height': `${Math.round(baseSize / aspectRatio)}px`
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
'--width': `${Math.round(baseSize * aspectRatio)}px`,
|
||||
'--height': `${baseSize}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.choice-btn{
|
||||
display: flex;
|
||||
height: 40px;
|
||||
padding: 0 15px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #E8E9EB;
|
||||
background: #f5f6f7;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.choice-btn:hover{
|
||||
background: #e9eaeb;
|
||||
}
|
||||
|
||||
.proportion-container{
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section{
|
||||
margin-bottom: 20px;
|
||||
border-radius: 20px;
|
||||
|
||||
&:last-child{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h3{
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
.proportion-options{
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
background-color: #F8F9FA;
|
||||
padding: 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.proportion-item{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
padding: 5px;
|
||||
width: auto;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 5px;
|
||||
text-align: bottom;
|
||||
color: #999;
|
||||
|
||||
&::before{
|
||||
content: '';
|
||||
width: var(--width, 20px);
|
||||
height: var(--height, 20px);
|
||||
background: #F5F6F7;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
border: 2px solid #999;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
&.active{
|
||||
color: #000F33;
|
||||
background: #ffffff;
|
||||
}
|
||||
&.active::before{
|
||||
border-color: #000F33;
|
||||
}
|
||||
}
|
||||
|
||||
.resolution-options{
|
||||
display: flex;
|
||||
padding: 5px;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
border-radius: 10px;
|
||||
background: #F8F9FA;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.resolution-item{
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
transition: all 0.2s ease;
|
||||
color: #666;
|
||||
|
||||
&:hover{
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
&.active{
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
<template>
|
||||
<div class="video-image-uploader">
|
||||
<div class="image-list">
|
||||
<div
|
||||
v-for="(item, index) in localPreviewList"
|
||||
:key="item.uid"
|
||||
class="image-item"
|
||||
>
|
||||
<img :src="item.url" class="uploaded-image" :alt="getFrameLabel(index)" />
|
||||
<div class="frame-label">{{ getFrameLabel(index) }}</div>
|
||||
<div class="delete-icon" @click.stop="handleDelete(index)">
|
||||
<i-ep-close />
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="i in maxImages" :key="i">
|
||||
<div
|
||||
v-if="localPreviewList.length < maxImages && !localPreviewList[i - 1]"
|
||||
class="upload-trigger"
|
||||
@click="triggerUpload(i - 1)"
|
||||
>
|
||||
<i-ep-plus color="#333333" />
|
||||
<div class="upload-text">{{ getUploadText(i) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<el-upload
|
||||
v-for="i in maxImages"
|
||||
:key="i"
|
||||
:ref="el => setUploadRef(el, i - 1)"
|
||||
v-show="false"
|
||||
:action="uploadurl"
|
||||
:limit="1"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="(response, uploadFile) => handleSuccess(response, uploadFile, i - 1)"
|
||||
:on-error="handleError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { genFileId } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
modelType: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
imagesCount: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const uploadurl = import.meta.env.VITE_API_WORKFLOW_UPLOAD
|
||||
const uploadRefs = ref([])
|
||||
const imageList = ref([...props.modelValue])
|
||||
const localPreviewList = ref([...props.modelValue])
|
||||
const isUploading = ref(false)
|
||||
|
||||
const showEndFrame = computed(() => props.modelType === 'image' && props.imagesCount === 2)
|
||||
const maxImages = computed(() => props.modelType === 'image' ? (showEndFrame.value ? 2 : 1) : 1)
|
||||
|
||||
const setUploadRef = (el, index) => {
|
||||
if (el) {
|
||||
uploadRefs.value[index] = el
|
||||
}
|
||||
}
|
||||
|
||||
const getFrameLabel = (index) => {
|
||||
if (props.modelType === 'digitalHuman') {
|
||||
return ''
|
||||
}
|
||||
return index === 0 ? '首帧' : '尾帧'
|
||||
}
|
||||
|
||||
const getUploadText = (i) => {
|
||||
if (props.modelType === 'digitalHuman') {
|
||||
return '参考内容'
|
||||
}
|
||||
return i === 1 ? '首帧' : '尾帧'
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, async (newVal) => {
|
||||
if (isUploading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
imageList.value = [...newVal]
|
||||
|
||||
const newPreviewList = []
|
||||
for (const img of newVal) {
|
||||
let previewImg = { ...img }
|
||||
if (img.url && !img.url.startsWith('blob:')) {
|
||||
try {
|
||||
const response = await fetch(img.url)
|
||||
const blob = await response.blob()
|
||||
previewImg = {
|
||||
...img,
|
||||
url: URL.createObjectURL(blob),
|
||||
serverUrl: img.url
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create blob URL:', error)
|
||||
previewImg = {
|
||||
...img,
|
||||
serverUrl: img.url
|
||||
}
|
||||
}
|
||||
}
|
||||
newPreviewList.push(previewImg)
|
||||
}
|
||||
localPreviewList.value = newPreviewList
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
const triggerUpload = (index) => {
|
||||
if (uploadRefs.value[index]) {
|
||||
uploadRefs.value[index].$el.querySelector('input').click()
|
||||
}
|
||||
}
|
||||
|
||||
const beforeUpload = (rawFile) => {
|
||||
const allowedTypes = ['image/jpeg', 'image/png']
|
||||
|
||||
if (!allowedTypes.includes(rawFile.type)) {
|
||||
ElMessage.error('不支持的文件类型,请上传 JPG、PNG 格式的图片')
|
||||
return false
|
||||
}
|
||||
|
||||
if (rawFile.size / 1024 / 1024 > 10) {
|
||||
ElMessage.error('图片大小不能超过 10MB')
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSuccess = (response, uploadFile, index) => {
|
||||
ElMessage.success('上传成功')
|
||||
|
||||
isUploading.value = true
|
||||
|
||||
const localUrl = URL.createObjectURL(uploadFile.raw)
|
||||
|
||||
const newImage = {
|
||||
uid: uploadFile.uid,
|
||||
url: response.url
|
||||
}
|
||||
|
||||
if (imageList.value[index]) {
|
||||
const previewItem = localPreviewList.value[index]
|
||||
if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(previewItem.url)
|
||||
}
|
||||
imageList.value[index] = newImage
|
||||
localPreviewList.value[index] = {
|
||||
uid: uploadFile.uid,
|
||||
url: localUrl,
|
||||
serverUrl: response.url
|
||||
}
|
||||
} else {
|
||||
imageList.value[index] = newImage
|
||||
localPreviewList.value[index] = {
|
||||
uid: uploadFile.uid,
|
||||
url: localUrl,
|
||||
serverUrl: response.url
|
||||
}
|
||||
}
|
||||
|
||||
emit('update:modelValue', [...imageList.value])
|
||||
|
||||
nextTick(() => {
|
||||
isUploading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const handleError = () => {
|
||||
ElMessage.error('上传失败,请重新选择图片')
|
||||
}
|
||||
|
||||
const handleDelete = (index) => {
|
||||
const previewItem = localPreviewList.value[index]
|
||||
if (previewItem && previewItem.url && previewItem.url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(previewItem.url)
|
||||
}
|
||||
|
||||
localPreviewList.value.splice(index, 1)
|
||||
imageList.value.splice(index, 1)
|
||||
emit('update:modelValue', [...imageList.value])
|
||||
}
|
||||
|
||||
const handleSelect = async (url, index = 0) => {
|
||||
const initialFile = await fetch(url)
|
||||
const blob = await initialFile.blob()
|
||||
const file = new File([blob], `selected_image_${Date.now()}.jpg`, { type: blob.type })
|
||||
file.uid = genFileId()
|
||||
|
||||
if (uploadRefs.value[index]) {
|
||||
uploadRefs.value[index].handleStart(file)
|
||||
uploadRefs.value[index].submit()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handleSelect
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.video-image-uploader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
position: relative;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
z-index: 10;
|
||||
|
||||
.delete-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.uploaded-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.frame-label {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
width: 32px;
|
||||
height: 20px;
|
||||
background: rgba(0, 15, 51, 0.8);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
z-index: 5;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 20;
|
||||
|
||||
&:hover {
|
||||
background: #dc2626;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-trigger {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: rgba(0, 15, 51, 0.04);
|
||||
backdrop-filter: blur(5px);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
font-size: 15px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
span {
|
||||
color: #333;
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #F0F1F2;
|
||||
border-color: #626aef;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
color: #666;
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -12,6 +12,6 @@
|
|||
{ "value": "Jimeng_4.0", "label": "Jimeng.4.0" }
|
||||
],
|
||||
"vision": [
|
||||
{ "value": "Qwen3.5plus", "label": "Qwen3.5plus" }
|
||||
{ "value": "Qwen3.5plus", "label": "Qwen3.5plus", "disabled": true }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,24 @@
|
|||
{
|
||||
"video": [
|
||||
{ "value": "FlashHead", "label": "FlashHead" },
|
||||
{ "value": "LTX2.3-T2V", "label": "LTX 2.3-T2V" },
|
||||
{ "value": "Vidu Q3-I2V", "label": "Vidu Q3-I2V" },
|
||||
{ "value": "Vidu Q3-T2V", "label": "Vidu Q3-T2V" }
|
||||
"文生视频": [
|
||||
{ "value": "LTX2.0", "label": "LTX2.0 T2V" },
|
||||
{ "value": "viduQ3-T2V", "label": "viduQ3 T2V" }
|
||||
],
|
||||
"首尾帧": [
|
||||
{ "value": "Hailuo-02-fast", "label": "海螺 fast" },
|
||||
{ "value": "LTX2.0-I2V", "label": "LTX2.0 I2V" },
|
||||
{ "value": "LTX2.3-T2V", "label": "LTX2.3 T2V", "disabled": true },
|
||||
{ "value": "ViduQ3-turbo", "label": "ViduQ3-turbo" }
|
||||
],
|
||||
"数字人": [
|
||||
{ "value": "FlashHead", "label": "FlashHead" }
|
||||
],
|
||||
"全能参考": [
|
||||
{ "value": "Seedance 2.0", "label": "Seedance 2.0", "disabled": true }
|
||||
],
|
||||
"智能多帧": [
|
||||
{ "value": "Seedance 2.0", "label": "Seedance 2.0", "disabled": true }
|
||||
],
|
||||
"主体参考": [
|
||||
{ "value": "Seedance 2.0", "label": "Seedance 2.0", "disabled": true }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
export async function Playload(data,modelType) {
|
||||
// data = getWidthHeight(data)
|
||||
import { fetchModelConfig } from '@/utils/modelConfig'
|
||||
|
||||
export async function Playload(data) {
|
||||
try {
|
||||
const response = await fetch(`https://resources.xueai.art/AIGC/static/public/Platform/Painting/workflows/${modelType}/${data.modelName}.json`)
|
||||
const json = await response.json()
|
||||
const json = await fetchModelConfig(data.type, data.modelName, data.modelType)
|
||||
|
||||
const nodeInfoList = []
|
||||
const proportionParam = data.params.find(param => param.name === 'proportion') || {data: 0}
|
||||
const resolutionParam = data.params.find(param => param.name === 'resolution') || {data: 0}
|
||||
|
||||
if (Array.isArray(data.imgs) && data.imgs.length > 0 && (modelType === 'image' || modelType === 'edit')) {
|
||||
if (Array.isArray(data.imgs) && data.imgs.length > 0 && (data.modelType === 'image' || data.modelType === 'edit')) {
|
||||
for (const key of data.imgs) {
|
||||
if (json.nodeInfoList[key.name]) {
|
||||
console.log(key)
|
||||
|
|
@ -27,9 +29,30 @@ export async function Playload(data,modelType) {
|
|||
}
|
||||
}
|
||||
|
||||
if ((json.nodeInfoList.width || json.nodeInfoList.height) && (proportionParam.data && resolutionParam.data)) {
|
||||
const { width, height } = getWidthHeight({
|
||||
proportion: proportionParam.data,
|
||||
resolution: resolutionParam.data
|
||||
})
|
||||
json.nodeInfoList.width.fieldValue = width
|
||||
json.nodeInfoList.height.fieldValue = height
|
||||
nodeInfoList.push(json.nodeInfoList.width, json.nodeInfoList.height)
|
||||
}
|
||||
|
||||
if (Array.isArray(json.seed)) {
|
||||
const min = Math.pow(10, 0)
|
||||
const max = Math.pow(10, 9) - 1
|
||||
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min
|
||||
json.seed.map((seedItem) => {
|
||||
seedItem.fieldValue = randomNum
|
||||
nodeInfoList.push(seedItem)
|
||||
})
|
||||
}
|
||||
|
||||
if (Array.isArray(json.must)) {
|
||||
nodeInfoList.push(...json.must)
|
||||
}
|
||||
|
||||
return {
|
||||
workflowId: json.workflowId,
|
||||
nodeInfoList
|
||||
|
|
@ -47,21 +70,23 @@ export function result(result) {
|
|||
return { type: false, message: result.data.exception_message }
|
||||
}
|
||||
function getWidthHeight(data) {
|
||||
// 去除分辨率字符串中的'p'并转换为数字
|
||||
// const resolution = 720
|
||||
const resolution = Number.parseInt(data.resolution.replace('p', '')) || Number.parseInt(data.resolution)
|
||||
// 解析宽高比
|
||||
const aspectRatioParts = data.aspect_ratio.split(':') || data.aspect_ratio.split(':')
|
||||
const widthRatio = Number.parseInt(aspectRatioParts[0])
|
||||
const heightRatio = Number.parseInt(aspectRatioParts[1])
|
||||
if (widthRatio > heightRatio) {
|
||||
data.height = resolution
|
||||
data.width = Math.round(resolution * widthRatio / heightRatio)
|
||||
} else {
|
||||
data.width = resolution
|
||||
data.height = Math.round(resolution * heightRatio / widthRatio)
|
||||
}
|
||||
console.log(data.width, data.height)
|
||||
let resolution = data.resolution * 1
|
||||
|
||||
return data
|
||||
let widthRatio = 16
|
||||
let heightRatio = 9
|
||||
if (data.proportion) {
|
||||
const aspectRatioParts = data.proportion.split(':') || data.proportion.split(':')
|
||||
widthRatio = Number.parseInt(aspectRatioParts[0]) || 16
|
||||
heightRatio = Number.parseInt(aspectRatioParts[1]) || 9
|
||||
}
|
||||
let width, height
|
||||
if (widthRatio > heightRatio) {
|
||||
height = resolution
|
||||
width = Math.round(resolution * widthRatio / heightRatio)
|
||||
} else {
|
||||
width = resolution
|
||||
height = Math.round(resolution * heightRatio / widthRatio)
|
||||
}
|
||||
console.log(width, height)
|
||||
return { width, height }
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import {
|
|||
getUserInfo as getUserInfoApi,
|
||||
logout as logoutApi
|
||||
} from '@/apis/auth'
|
||||
import { getFreeTimes } from '@/apis/display'
|
||||
import { clearToken, getToken, setToken } from '@/utils/auth'
|
||||
|
||||
const storeSetup = () => {
|
||||
|
|
@ -33,6 +34,7 @@ const storeSetup = () => {
|
|||
|
||||
const dept = ref({}) // 当前用户所在部门集合
|
||||
const isLogin = ref(false)
|
||||
const freeTimes = ref(0) // 免费次数
|
||||
|
||||
// 重置token
|
||||
const resetToken = () => {
|
||||
|
|
@ -70,6 +72,18 @@ const storeSetup = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 获取免费次数
|
||||
const fetchFreeTimes = async (chargeType = 1) => {
|
||||
if (userInfo.id) {
|
||||
const res = await getFreeTimes(userInfo.id)
|
||||
const balanceList = res.data || []
|
||||
const target = balanceList.find((item) => item.chargeType === chargeType)
|
||||
freeTimes.value = target?.balance || 0
|
||||
return freeTimes.value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 登录
|
||||
const accountLogin = async (req) => {
|
||||
const res = await accountLoginApi(req)
|
||||
|
|
@ -120,12 +134,14 @@ const storeSetup = () => {
|
|||
dept,
|
||||
username,
|
||||
isLogin,
|
||||
freeTimes,
|
||||
accountLogin,
|
||||
logout,
|
||||
logoutCallBack,
|
||||
getInfo,
|
||||
resetToken,
|
||||
checkTokenValid
|
||||
checkTokenValid,
|
||||
fetchFreeTimes
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import outPlatform from '@/config/index'
|
||||
|
||||
// 处理音频生成任务的数据并返回
|
||||
export async function createTask(data, modelType, taskId, token) {
|
||||
console.log(data, modelType)
|
||||
const payload = await outPlatform[data.platform].Playload(data, modelType)
|
||||
export async function createTask(data, taskId, token) {
|
||||
console.log(data)
|
||||
const payload = await outPlatform[data.platform].Playload(data)
|
||||
|
||||
return {
|
||||
AIGC: data.AIGC,
|
||||
platform: data.platform,
|
||||
prompt: data.prompt,
|
||||
taskType: modelType === 'text' ? 1 : 2,
|
||||
taskType: data.modelType === 'text' ? 1 : 2,
|
||||
modelName: data.modelName,
|
||||
payload,
|
||||
taskId,
|
||||
token,
|
||||
quantity: data.quantity,
|
||||
free: data.free,
|
||||
result: data.result
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
const STORAGE_PREFIX = 'model_config_'
|
||||
|
||||
function getTodayDateString() {
|
||||
const today = new Date()
|
||||
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function getStorageKey(modelName, modelType) {
|
||||
return `${STORAGE_PREFIX}${modelType}_${modelName}`
|
||||
}
|
||||
|
||||
function getConfigFromStorage(modelName, modelType) {
|
||||
try {
|
||||
const key = getStorageKey(modelName, modelType)
|
||||
const stored = localStorage.getItem(key)
|
||||
|
||||
if (!stored) {
|
||||
return null
|
||||
}
|
||||
|
||||
const data = JSON.parse(stored)
|
||||
const todayStr = getTodayDateString()
|
||||
|
||||
if (data.storageDate !== todayStr) {
|
||||
localStorage.removeItem(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return data.config
|
||||
} catch (error) {
|
||||
console.error('从localStorage读取配置失败:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfigToStorage(modelName, modelType, config) {
|
||||
try {
|
||||
const key = getStorageKey(modelName, modelType)
|
||||
const data = {
|
||||
config,
|
||||
storageDate: getTodayDateString(),
|
||||
timestamp: Date.now()
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(data))
|
||||
} catch (error) {
|
||||
console.error('保存配置到localStorage失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchModelConfig(type, modelName, modelType) {
|
||||
const cachedConfig = getConfigFromStorage(modelName, modelType)
|
||||
|
||||
if (cachedConfig) {
|
||||
console.log(`从缓存加载模型配置: ${modelName}`)
|
||||
return cachedConfig
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `${import.meta.env.VITE_API_MODEL_RESOURCE}/static/public/Platform/${type}/workflows/${modelType}/${modelName}.json`
|
||||
console.log(`从远程获取模型配置: ${url}`)
|
||||
|
||||
const response = await fetch(url)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const config = await response.json()
|
||||
|
||||
saveConfigToStorage(modelName, modelType, config)
|
||||
|
||||
return config
|
||||
} catch (error) {
|
||||
console.error('获取模型配置失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function clearModelConfigCache(modelName, modelType) {
|
||||
const key = getStorageKey(modelName, modelType)
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
export function clearAllModelConfigCache() {
|
||||
const keysToRemove = []
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i)
|
||||
if (key && key.startsWith(STORAGE_PREFIX)) {
|
||||
keysToRemove.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key))
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { ElNotification } from 'element-plus'
|
||||
import { h, ref } from 'vue'
|
||||
import { useDisplayStore } from '@/stores'
|
||||
import { useDisplayStore, useUserStore } from '@/stores'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { createTask, getTask } from '@/utils/createTask'
|
||||
import { userError } from '@/utils/tokenError'
|
||||
|
||||
export function getChargeType(chargeType) {
|
||||
switch (chargeType) {
|
||||
case 'painting':
|
||||
case 'Painting':
|
||||
return 1
|
||||
case 'video':
|
||||
case 'Video':
|
||||
return 4
|
||||
default:
|
||||
return 2
|
||||
|
|
@ -58,7 +58,7 @@ export function websocketSuccess() {
|
|||
})
|
||||
}
|
||||
|
||||
export async function generate(modelType, data, generateData, type) {
|
||||
export async function generate(data, generateData) {
|
||||
const progress_text = ref('')
|
||||
const message = ref('')
|
||||
const useDisplay = useDisplayStore()
|
||||
|
|
@ -68,7 +68,7 @@ export async function generate(modelType, data, generateData, type) {
|
|||
|
||||
useDisplay.isSubGerenate = true
|
||||
|
||||
const result = await createTask(data, modelType, taskId, token)
|
||||
const result = await createTask(data, taskId, token)
|
||||
console.log(result)
|
||||
// const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjY0NDEwODAyMjk1OTgzNzIzMCwicm5TdHIiOiJiWkVwS2JLWFJyZmRIaFFHWXZKTkdzOGdGM0JSRmxQOCJ9.5eQ2GtVdrDntQDe2tnF8vl_DhTfd2uW-KNqzvl1imc0'
|
||||
const wsURL = `${import.meta.env.VITE_API_WORKFLOW_WS}/?token=Bearer ${token}`
|
||||
|
|
@ -102,7 +102,7 @@ export async function generate(modelType, data, generateData, type) {
|
|||
|
||||
useDisplay.addGeneratingItem({
|
||||
taskId: taskId,
|
||||
type: type,
|
||||
type: data.type,
|
||||
generateData: generateData
|
||||
})
|
||||
setTimeout(() => {
|
||||
|
|
@ -145,8 +145,8 @@ export async function generate(modelType, data, generateData, type) {
|
|||
} else if (event.code === 1000 && event.reason === 'success') {
|
||||
console.log('收到服务器消息:', res)
|
||||
const result = await getTask(res)
|
||||
if(useUserStore().freeTimes) await useUserStore().fetchFreeTimes()
|
||||
if (result.type) {
|
||||
|
||||
if (currentTaskId) {
|
||||
useDisplay.updateItemToSuccess(currentTaskId, result.urls)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
<div style="width: 100%;display: flex;justify-content: center;align-items: center;transform: rotate(180deg);">
|
||||
<div class="primary-box" :class="{ 'none-primary-box': props.item.status === 'none' }">
|
||||
|
||||
<div class="prompt-container" ref="promptContainerRef" @mouseenter="isHovering = true" @mouseleave="isHovering = false">
|
||||
<div class="prompt-container" ref="promptContainerRef">
|
||||
<div class="prompt-wrapper" ref="promptWrapperRef">
|
||||
<div class="prompt" ref="promptRef" :class="{ 'expanded': isHovering }">
|
||||
<span class="prompt-text">{{ props.item.generateData.prompt || '生成图片' }}</span>
|
||||
<span class="prompt-text" @mouseenter="isHovering = true" @mouseleave="isHovering = false">{{ props.item.generateData.prompt || '生成图片' }}</span>
|
||||
<div class="generate-data internal" v-show="!isHovering && !showExternalGenerateData">
|
||||
<div class="detailed-data first-detailed-data">{{ props.item.generateData.model }}</div>
|
||||
<div class="detailed-data">{{ props.item.generateData.proportion }}</div>
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
</div>
|
||||
|
||||
<!-- 已完成 图片 -->
|
||||
<div v-if="props.item.status === 'success' && props.item.type === 'painting'" class="box success-box">
|
||||
<div v-if="props.item.status === 'success' && props.item.type === 'Painting'" class="box success-box">
|
||||
<div v-for="(file, index) in props.item.files" :key="index" class="one-box" :class="{ 'collected': isCollected(file) }" @mouseenter="hoverIndex = index" @mouseleave="hoverIndex = -1">
|
||||
<!-- <img :src="file" alt="index" class="img" /> -->
|
||||
<Img :src="file" alt="index" class="img" />
|
||||
|
|
@ -70,10 +70,10 @@
|
|||
</div>
|
||||
|
||||
<!-- 已完成 视频 -->
|
||||
<div v-if="props.item.status === 'success' && props.item.type === 'video'" class="box success-box">
|
||||
<div v-if="props.item.status === 'success' && props.item.type === 'Video'" class="box success-box">
|
||||
<div class="one-box" :class="{ 'collected': isCollected(props.item.files[0]) }" @mouseenter="hoverIndex = 0" @mouseleave="hoverIndex = -1">
|
||||
<!-- <img :src="file" alt="index" class="img" /> -->
|
||||
<video :src="props.item.files[0]" alt="index" class="video" />
|
||||
<video :src="props.item.files[0]" class="video" controls playsinline />
|
||||
|
||||
<div class="left-top">
|
||||
<div v-show="hoverIndex === 0" class="left-top-btn download-btn" @click="downloadImage(props.item.files[0], 'video')"><img src="@/assets/display/download.svg" /></div>
|
||||
|
|
@ -273,7 +273,7 @@ const addCollection = async (url) => {
|
|||
const res = await cancelOrCollect({
|
||||
taskId: props.item.id,
|
||||
userId: useUser.userInfo.id,
|
||||
url: url
|
||||
url: url,
|
||||
})
|
||||
if (res.success) {
|
||||
ElMessage.success(res.message || '操作成功')
|
||||
|
|
@ -328,6 +328,7 @@ const addCollection = async (url) => {
|
|||
top: 0;
|
||||
width: 102%;
|
||||
z-index: 5;
|
||||
width: auto;
|
||||
|
||||
&.expanded{
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ const props = defineProps({
|
|||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'painting'
|
||||
default: 'Painting'
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -179,7 +179,7 @@ const fetchHistory = async (isLoadMore = false) => {
|
|||
refreshing.value = false
|
||||
isInitializing.value = false
|
||||
useDisplay.Sender_variant = 'updown'
|
||||
router.push({ name: 'generate' })
|
||||
router.push({ name: 'generate', query: { type: props.type } })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const dialogBoxRef = ref(null)
|
|||
const shouldShowDisplay = computed(() => route.path === '/home')
|
||||
const loading = computed(() => route.query.loading ? false : (route.path === '/home'))
|
||||
const Generate = computed(() => route.query.Generate || false)
|
||||
const type = computed(() => route.query.type || 'painting')
|
||||
const type = computed(() => route.query.type || 'Painting')
|
||||
console.log(type.value)
|
||||
|
||||
watch(dialogBoxRef, (newRef) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue