mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-05-10 11:44:28 +08:00
Jimeng VirtualHuman and actionTransfer is ready
This commit is contained in:
469
web/src/components/FileUpload.vue
Normal file
469
web/src/components/FileUpload.vue
Normal file
@@ -0,0 +1,469 @@
|
||||
<template>
|
||||
<div class="file__upload-container">
|
||||
<!-- 单文件模式 -->
|
||||
<template v-if="props.maxCount === 1">
|
||||
<div class="single-upload">
|
||||
<div v-if="fileList.length === 0" class="upload-btn upload-btn-single">
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:multiple="false"
|
||||
:accept="accept"
|
||||
class="uploader"
|
||||
>
|
||||
<div class="upload-placeholder">
|
||||
<el-icon :size="20"><UploadFilled /></el-icon>
|
||||
<span>上传文件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="relative inline-flex items-center border border-gray-200 rounded-xl bg-white dark:bg-[#2b2b2b] dark:border-gray-700 p-2 w-full"
|
||||
>
|
||||
<img :src="getFileImage(fileList[0].url)" class="w-10 h-10 mr-2" />
|
||||
<div class="min-w-0 flex flex-col items-center gap-1 text-sm">
|
||||
<a
|
||||
:href="fileList[0].url"
|
||||
target="_blank"
|
||||
class="truncate block text-[var(--theme-text-color-primary,#0d0d0d)] max-w-[220px]"
|
||||
>
|
||||
{{ fileList[0].name }}
|
||||
</a>
|
||||
<div class="text-xs flex w-full justify-start text-gray-500">
|
||||
{{ GetFileType(getFileExt(fileList[0].name)) }} ·
|
||||
{{ FormatFileSize(fileList[0].size || 0) }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="absolute -right-2 -top-2 w-5 h-5 rounded-full bg-rose-600 text-white flex items-center justify-center text-[10px]"
|
||||
@click="removeFile(0)"
|
||||
aria-label="remove"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 多文件模式 -->
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-2 px-2 pt-2 !items-start" v-if="fileList.length > 0">
|
||||
<div
|
||||
v-for="(file, index) in fileList"
|
||||
:key="file.url || index"
|
||||
class="relative inline-flex items-center border border-gray-200 rounded-xl bg-white dark:bg-[#2b2b2b] dark:border-gray-700 p-2 w-full"
|
||||
>
|
||||
<img :src="getFileImage(file.url)" class="w-10 h-10 mr-2" />
|
||||
<div class="min-w-0 flex flex-col items-center gap-1 text-sm">
|
||||
<a :href="file.url" target="_blank" class="truncate block max-w-[180px]">{{
|
||||
file.name
|
||||
}}</a>
|
||||
<div class="text-xs flex w-full justify-start text-gray-500">
|
||||
{{ GetFileType(getFileExt(file.name)) }} · {{ FormatFileSize(file.size || 0) }}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="absolute -right-2 -top-2 w-5 h-5 rounded-full bg-rose-600 text-white flex items-center justify-center text-[10px]"
|
||||
@click="removeFile(index)"
|
||||
aria-label="remove"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<!-- 上传按钮 -->
|
||||
<div v-if="!multiple || fileList.length < maxCount" class="upload-btn">
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
class="uploader"
|
||||
:limit="maxCount"
|
||||
>
|
||||
<div class="upload-placeholder">
|
||||
<el-icon :size="20"><UploadFilled /></el-icon>
|
||||
<span>上传文件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 初始上传区域 -->
|
||||
<div v-else class="upload-area">
|
||||
<el-upload
|
||||
drag
|
||||
:auto-upload="true"
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
class="uploader"
|
||||
:limit="maxCount"
|
||||
>
|
||||
<el-icon :size="40" class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">拖拽文件到此处,或 <em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip text-gray-500 text-sm">
|
||||
支持 {{ accept }} 格式,最多上传 {{ maxCount }} 个,单个最大 {{ maxSize }}MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 上传进度 -->
|
||||
<el-progress
|
||||
v-if="uploading"
|
||||
:percentage="uploadProgress"
|
||||
:stroke-width="4"
|
||||
class="upload-progress"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { FormatFileSize, GetFileIcon, GetFileType } from '@/store/system'
|
||||
import { httpPost } from '@/utils/http'
|
||||
import { isImage, replaceImg } from '@/utils/libs'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Array],
|
||||
default: '',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxCount: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.md,.zip,.rar,.7z',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'upload-success', 'remove-file'])
|
||||
|
||||
// 上传状态
|
||||
const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const FileInfoList = ref([])
|
||||
|
||||
// 文件列表
|
||||
const fileList = computed({
|
||||
get() {
|
||||
if (props.multiple || props.maxCount > 1) {
|
||||
return FileInfoList.value
|
||||
} else {
|
||||
return FileInfoList.value && FileInfoList.value.length > 0 ? FileInfoList.value : []
|
||||
}
|
||||
},
|
||||
set(value) {
|
||||
const isMulti = props.multiple || props.maxCount > 1
|
||||
const normalized = Array.isArray(value) ? value : value ? [value] : []
|
||||
FileInfoList.value = normalized
|
||||
if (isMulti) {
|
||||
const urls = normalized.map((v) => v && v.url).filter((u) => !!u)
|
||||
emit('update:modelValue', urls)
|
||||
} else {
|
||||
const url =
|
||||
normalized.length > 0 && normalized[0] && normalized[0].url ? normalized[0].url : ''
|
||||
emit('update:modelValue', url)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const uploadCount = ref(1)
|
||||
|
||||
// 获取文件扩展名
|
||||
const getFileExt = (filename) => {
|
||||
return '.' + filename.split('.').pop().toLowerCase()
|
||||
}
|
||||
|
||||
const getFileName = (url) => {
|
||||
return url.split('/').pop()
|
||||
}
|
||||
|
||||
// 获取文件
|
||||
const getFileImage = (url) => {
|
||||
return isImage(url) ? url : GetFileIcon(getFileExt(url))
|
||||
}
|
||||
|
||||
// 将外部 modelValue 同步为内部文件对象列表
|
||||
const urlToFileInfo = (url) => ({
|
||||
url,
|
||||
name: getFileName(url),
|
||||
size: 0,
|
||||
ext: getFileExt(url),
|
||||
})
|
||||
|
||||
// 通过 HEAD 请求尝试获取远程资源大小
|
||||
const fetchRemoteFileSize = async (url) => {
|
||||
try {
|
||||
const res = await fetch(url, { method: 'HEAD' })
|
||||
const len = res.headers.get('content-length')
|
||||
return len ? parseInt(len, 10) : 0
|
||||
} catch (e) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 对 size 为空或 0 的项进行补充
|
||||
const updateUnknownSizes = async (items) => {
|
||||
const tasks = items.map(async (it) => {
|
||||
if (!it || !it.url) return it
|
||||
if (!it.size || it.size === 0) {
|
||||
const s = await fetchRemoteFileSize(it.url)
|
||||
if (s > 0) {
|
||||
it.size = s
|
||||
}
|
||||
}
|
||||
return it
|
||||
})
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
const isMulti = props.multiple || props.maxCount > 1
|
||||
if (isMulti) {
|
||||
const urls = Array.isArray(newVal) ? newVal : []
|
||||
FileInfoList.value = urls.map((u) => urlToFileInfo(u))
|
||||
// 异步补齐大小
|
||||
updateUnknownSizes(FileInfoList.value)
|
||||
} else {
|
||||
const url = typeof newVal === 'string' ? newVal : ''
|
||||
FileInfoList.value = url ? [urlToFileInfo(url)] : []
|
||||
// 异步补齐大小
|
||||
updateUnknownSizes(FileInfoList.value)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 处理上传
|
||||
const handleUpload = async (uploadFile) => {
|
||||
const file = uploadFile.file
|
||||
// 检查文件大小
|
||||
if (file.size > props.maxSize * 1024 * 1024) {
|
||||
ElMessage.error(`文件大小不能超过 ${props.maxSize}MB`)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查数量限制
|
||||
if (uploadCount.value > props.maxCount) {
|
||||
ElMessage.error(`最多只能上传 ${props.maxCount} 个文件`)
|
||||
return
|
||||
}
|
||||
uploadCount.value++
|
||||
|
||||
uploading.value = true
|
||||
uploadProgress.value = 0
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
// 模拟上传进度
|
||||
const progressTimer = setInterval(() => {
|
||||
if (uploadProgress.value < 90) {
|
||||
uploadProgress.value += 10
|
||||
}
|
||||
}, 100)
|
||||
|
||||
const response = await httpPost('/api/upload', formData)
|
||||
|
||||
clearInterval(progressTimer)
|
||||
uploadProgress.value = 100
|
||||
|
||||
const fileUrl = replaceImg(response.data.url)
|
||||
const fileInfo = {
|
||||
name: file.name,
|
||||
url: fileUrl,
|
||||
size: file.size,
|
||||
ext: getFileExt(file.name),
|
||||
}
|
||||
|
||||
// 更新文件列表
|
||||
if (props.multiple || props.maxCount > 1) {
|
||||
const newList = [...fileList.value, fileInfo]
|
||||
fileList.value = newList
|
||||
} else {
|
||||
fileList.value = [fileInfo]
|
||||
}
|
||||
|
||||
emit('upload-success', fileInfo)
|
||||
ElMessage.success('上传成功')
|
||||
} catch (error) {
|
||||
ElMessage.error('上传失败: ' + (error.message || '网络错误'))
|
||||
} finally {
|
||||
uploading.value = false
|
||||
uploadProgress.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
const removeFile = (index) => {
|
||||
const file = fileList.value[index]
|
||||
const newList = [...fileList.value]
|
||||
newList.splice(index, 1)
|
||||
fileList.value = newList
|
||||
uploadCount.value--
|
||||
emit('remove-file', file)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.file__upload-container {
|
||||
width: 100%;
|
||||
|
||||
.single-upload {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.upload-btn-single {
|
||||
.uploader {
|
||||
width: 100%;
|
||||
|
||||
.el-upload-dragger {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .single-file-item {
|
||||
// width: 100%;
|
||||
// position: relative;
|
||||
// border-radius: 6px;
|
||||
// overflow: hidden;
|
||||
// border: 1px solid #dcdfe6;
|
||||
// background-color: var(--chat-content-bg, #f5f5f5);
|
||||
// padding: 8px;
|
||||
// }
|
||||
|
||||
.upload-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dcdfe6;
|
||||
background-color: var(--chat-content-bg, #f5f5f5);
|
||||
padding: 8px;
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
.el-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-left: 8px;
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
|
||||
.title {
|
||||
color: #0d0d0d;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #b4b4b4;
|
||||
|
||||
span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-overlay {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
opacity: 1;
|
||||
|
||||
.remove-btn {
|
||||
background: rgba(245, 108, 108, 0.8);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
.uploader {
|
||||
width: 100%;
|
||||
|
||||
.el-upload-dragger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
.el-upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.uploader {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="image-upload">
|
||||
<div class="image__upload-container">
|
||||
<!-- 单图模式 -->
|
||||
<template v-if="props.maxCount === 1">
|
||||
<div class="single-upload">
|
||||
@@ -59,7 +59,7 @@
|
||||
:show-file-list="false"
|
||||
:http-request="handleUpload"
|
||||
:multiple="multiple"
|
||||
accept="image/*"
|
||||
:accept="accept"
|
||||
class="uploader"
|
||||
:limit="maxCount"
|
||||
>
|
||||
@@ -157,7 +157,7 @@ const imageList = computed({
|
||||
},
|
||||
})
|
||||
|
||||
const uploadCount = ref(1)
|
||||
// 使用已选图片数量进行限制,不再使用全局计数
|
||||
// 处理上传
|
||||
const handleUpload = async (uploadFile) => {
|
||||
const file = uploadFile.file
|
||||
@@ -174,12 +174,11 @@ const handleUpload = async (uploadFile) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查数量限制
|
||||
if (uploadCount.value > props.maxCount) {
|
||||
// 检查数量限制(单图或多图)
|
||||
if ((props.multiple || props.maxCount > 1) && imageList.value.length >= props.maxCount) {
|
||||
ElMessage.error(`最多只能上传 ${props.maxCount} 张图片`)
|
||||
return
|
||||
}
|
||||
uploadCount.value++
|
||||
|
||||
uploading.value = true
|
||||
uploadProgress.value = 0
|
||||
@@ -225,111 +224,110 @@ const removeImage = (index) => {
|
||||
const newList = [...imageList.value]
|
||||
newList.splice(index, 1)
|
||||
imageList.value = newList
|
||||
uploadCount.value--
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.image-upload {
|
||||
.image__upload-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.single-upload {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.single-image-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.upload-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dcdfe6;
|
||||
|
||||
.upload-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.single-upload {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.upload-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
.single-image-item {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.upload-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
|
||||
.remove-btn {
|
||||
background: rgba(245, 108, 108, 0.8);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
.uploader {
|
||||
width: 100%;
|
||||
.upload-item {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #dcdfe6;
|
||||
|
||||
.el-upload-dragger {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
.upload-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
|
||||
.remove-btn {
|
||||
background: rgba(245, 108, 108, 0.8);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
.upload-btn {
|
||||
.uploader {
|
||||
width: 100%;
|
||||
|
||||
.el-upload-dragger {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
.el-upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.uploader {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
color: #8c939d;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
.el-upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.uploader {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,14 +22,15 @@
|
||||
<el-option v-for="item in items" :key="item.name" :label="item.name" :value="item">
|
||||
<div class="flex justify-start">
|
||||
<span
|
||||
class="flex items-center justify-center text-white !text-xl model-version mr-2 w-[40px] h-[40px] rounded-lg"
|
||||
>{{ item.version }}</span
|
||||
class="flex items-center justify-center text-white model-version mr-2 w-[40px] h-[40px] rounded-lg"
|
||||
:class="item.icon.size ? item.icon.size : '!text-xl'"
|
||||
>{{ item.icon.text }}</span
|
||||
>
|
||||
<div class="flex !items-start flex-col py-2 space-y-1">
|
||||
<span class="label text-sm">{{ item.name }}</span>
|
||||
<div class="whitespace-pre-line">
|
||||
<span
|
||||
class="text-xs text-gray-500 break-words line-clamp-1 max-w-[200px]"
|
||||
class="text-xs text-gray-500 break-words line-clamp-1 max-w-[250px]"
|
||||
:title="item.label"
|
||||
>{{ item.label }}</span
|
||||
>
|
||||
@@ -47,7 +48,10 @@
|
||||
</div>
|
||||
<p v-if="param.info" class="text-xs text-gray-500 mb-1">{{ param.info }}</p>
|
||||
</div>
|
||||
<div class="w-full flex flex-col !items-start space-y-2" v-else>
|
||||
<div
|
||||
class="w-full flex flex-col !items-start space-y-2"
|
||||
v-else-if="param.type !== 'hidden'"
|
||||
>
|
||||
<label class="label font-bold">
|
||||
{{ param.label }}
|
||||
<span v-if="param.required" class="text-red-500 ml-1">*</span>
|
||||
@@ -139,6 +143,14 @@
|
||||
:max-size="param.maxSize"
|
||||
:accept="param.accept"
|
||||
/>
|
||||
<FileUpload
|
||||
v-if="param.type === 'file'"
|
||||
v-model="modelValue[param.name]"
|
||||
:max-count="param.maxCount"
|
||||
:multiple="param.multiple"
|
||||
:max-size="param.maxSize"
|
||||
:accept="param.accept"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,6 +159,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FileUpload from './FileUpload.vue'
|
||||
import ImageUpload from './ImageUpload.vue'
|
||||
import ParamEmpty from './ui/ParamEmpty.vue'
|
||||
|
||||
@@ -225,7 +238,11 @@ const initModelValue = (model) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
// 初始化 req_key 和 action
|
||||
defaultValues.req_key = selectedModel.value.key
|
||||
defaultValues.action = selectedModel.value.action
|
||||
? selectedModel.value.action
|
||||
: 'CVSync2AsyncSubmitTask'
|
||||
return defaultValues
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,179 @@ import Spring_Festival_traditional_Chinese_architecture from '@/assets/img/jimen
|
||||
export const JimengParams = {
|
||||
image: [
|
||||
{
|
||||
name: '图片 4.0 文/图生图',
|
||||
version: '4.0',
|
||||
name: '即梦AI图片-4.0',
|
||||
icon: { text: '4.0' },
|
||||
label: '即梦4.0是即梦同源的图像生成能力,支持4K超高清输出',
|
||||
key: 'jimeng_t2i_v40',
|
||||
params: [
|
||||
{
|
||||
name: 'prompt',
|
||||
label: '提示词',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
showWordLimit: true,
|
||||
maxlength: 800,
|
||||
autosize: { minRows: 3, maxRows: 5 },
|
||||
placeholder: '请输入用于编辑图像的提示词,如:把xxx改成xxx,删除xxx,添加xxx等',
|
||||
info: '最长不超过800字符,prompt过长有概率出图异常或不生效',
|
||||
},
|
||||
{
|
||||
name: 'image_urls',
|
||||
label: '参考图片',
|
||||
type: 'image',
|
||||
required: false,
|
||||
placeholder: '请上传图片',
|
||||
maxSize: 10,
|
||||
multiple: true,
|
||||
maxCount: 10,
|
||||
accept: '.png,.jpg,.jpeg',
|
||||
info: '最大 15MB,支持最多输入10张图',
|
||||
},
|
||||
|
||||
// 图片比例
|
||||
{
|
||||
name: 'scale',
|
||||
type: 'slider',
|
||||
required: true,
|
||||
info: '该值越大代表文本描述影响程度越大,且输入图片影响程度越小',
|
||||
label: '文本影响力',
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
value: 0.5,
|
||||
},
|
||||
|
||||
// 是否强制生成单图
|
||||
{
|
||||
name: 'force_single',
|
||||
type: 'hidden',
|
||||
required: true,
|
||||
value: true,
|
||||
},
|
||||
|
||||
// 图片尺寸
|
||||
{
|
||||
name: 'size',
|
||||
type: 'select',
|
||||
required: true,
|
||||
placeholder: '请选择尺寸',
|
||||
label: '图片尺寸',
|
||||
prefix: 'icon-resize',
|
||||
options: [
|
||||
// 1K 分辨率
|
||||
{
|
||||
label: '1:1 (1024 x 1024)',
|
||||
value: '1024x1024',
|
||||
},
|
||||
{
|
||||
label: '4:3 (1152 x 864)',
|
||||
value: '1152x864',
|
||||
},
|
||||
{
|
||||
label: '3:4 (864 x 1152)',
|
||||
value: '864x1152',
|
||||
},
|
||||
{
|
||||
label: '3:2 (1248 x 832)',
|
||||
value: '1248x832',
|
||||
},
|
||||
{
|
||||
label: '2:3 (832 x 1248)',
|
||||
value: '832x1248',
|
||||
},
|
||||
{
|
||||
label: '16:9 (1280 x 720)',
|
||||
value: '1280x720',
|
||||
},
|
||||
{
|
||||
label: '9:16 (720 x 1280)',
|
||||
value: '720x1280',
|
||||
},
|
||||
{
|
||||
label: '21:9 (1512 x 648)',
|
||||
value: '1512x648',
|
||||
},
|
||||
{
|
||||
label: '9:21 (648 x 1512)',
|
||||
value: '648x1512',
|
||||
},
|
||||
// 2K 分辨率
|
||||
{
|
||||
label: '1:1 (2048 x 2048)',
|
||||
value: '2048x2048',
|
||||
},
|
||||
{
|
||||
label: '4:3 (2304 x 1728)',
|
||||
value: '2304x1728',
|
||||
},
|
||||
{
|
||||
label: '3:4 (1728 x 2304)',
|
||||
value: '1728x2304',
|
||||
},
|
||||
{
|
||||
label: '3:2 (2496 x 1664)',
|
||||
value: '2496x1664',
|
||||
},
|
||||
{
|
||||
label: '2:3 (1664 x 2496)',
|
||||
value: '1664x2496',
|
||||
},
|
||||
{
|
||||
label: '16:9 (2560 x 1440)',
|
||||
value: '2560x1440',
|
||||
},
|
||||
{
|
||||
label: '9:16 (1440 x 2560)',
|
||||
value: '1440x2560',
|
||||
},
|
||||
{
|
||||
label: '21:9 (3024 x 1296)',
|
||||
value: '3024x1296',
|
||||
},
|
||||
{
|
||||
label: '9:21 (1296 x 3024)',
|
||||
value: '1296x3024',
|
||||
},
|
||||
// 4K 分辨率
|
||||
{
|
||||
label: '1:1 (4096 x 4096)',
|
||||
value: '4096x4096',
|
||||
},
|
||||
{
|
||||
label: '4:3 (4736 x 3552)',
|
||||
value: '4736x3552',
|
||||
},
|
||||
{
|
||||
label: '3:4 (3552 x 4736)',
|
||||
value: '3552x4736',
|
||||
},
|
||||
{
|
||||
label: '3:2 (5024 x 3360)',
|
||||
value: '5024x3360',
|
||||
},
|
||||
{
|
||||
label: '2:3 (3360 x 5024)',
|
||||
value: '3360x5024',
|
||||
},
|
||||
{
|
||||
label: '16:9 (5472 x 2072)',
|
||||
value: '5472x2072',
|
||||
},
|
||||
{
|
||||
label: '9:16 (2072 x 5472)',
|
||||
value: '2072x5472',
|
||||
},
|
||||
{
|
||||
label: '21:9 (6272 x 2688)',
|
||||
value: '6272x2688',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '豆包-seedream-4.0',
|
||||
icon: { text: '4.0' },
|
||||
label: '支持文本、单图和多图输入,实现基于主体一致性的多图融合创作、图像编辑等多样玩法',
|
||||
key: 'doubao-seedream-4-0-250828',
|
||||
params: [
|
||||
@@ -105,7 +276,7 @@ export const JimengParams = {
|
||||
|
||||
{
|
||||
name: '图片 3.0 文生图',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '影视质感,文字更准,直出2k高清图',
|
||||
key: 'jimeng_t2i_v30',
|
||||
params: [
|
||||
@@ -182,7 +353,7 @@ export const JimengParams = {
|
||||
},
|
||||
{
|
||||
name: '图片 3.1 文生图',
|
||||
version: '3.1',
|
||||
icon: { text: '3.1' },
|
||||
label: '丰富的美学多样性,画面更鲜明生动',
|
||||
key: 'jimeng_t2i_v31',
|
||||
params: [
|
||||
@@ -260,7 +431,7 @@ export const JimengParams = {
|
||||
|
||||
{
|
||||
name: '图片 3.0 图生图',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '精准执行编辑指令,保持图像内容完整性',
|
||||
key: 'jimeng_i2i_v30',
|
||||
params: [
|
||||
@@ -343,7 +514,7 @@ export const JimengParams = {
|
||||
|
||||
{
|
||||
name: '图片 3.0 图像特效',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '将输入的单人写真图片,进行有创意的特效化处理。',
|
||||
key: 'i2i_multi_style_zx2x',
|
||||
params: [
|
||||
@@ -560,7 +731,7 @@ export const JimengParams = {
|
||||
|
||||
{
|
||||
name: '图片 2.1 文生图',
|
||||
version: '2.1',
|
||||
icon: { text: '2.1' },
|
||||
label: '平面绘感强,可生成文字海报',
|
||||
key: 'jimeng_high_aes_general_v21_L',
|
||||
params: [
|
||||
@@ -633,7 +804,7 @@ export const JimengParams = {
|
||||
// 视频 3.0 720P-文生视频
|
||||
{
|
||||
name: '视频 3.0 720P-文生视频',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '生成效果与速度兼备',
|
||||
key: 'jimeng_t2v_v30',
|
||||
params: [
|
||||
@@ -703,7 +874,7 @@ export const JimengParams = {
|
||||
// 视频 3.0 图生视频-首帧
|
||||
{
|
||||
name: '视频 3.0 720P-图生视频-首帧',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '根据提示词 + 首帧图片生成视频',
|
||||
key: 'jimeng_i2v_first_v30',
|
||||
params: [
|
||||
@@ -749,7 +920,7 @@ export const JimengParams = {
|
||||
// 视频 3.0 图生视频-首尾帧
|
||||
{
|
||||
name: '视频 3.0 720P-图生视频-首尾帧',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '根据提示词 + 首尾帧图片生成视频',
|
||||
key: 'jimeng_i2v_first_tail_v30',
|
||||
params: [
|
||||
@@ -796,7 +967,7 @@ export const JimengParams = {
|
||||
// 视频 3.0 图生视频-运镜
|
||||
{
|
||||
name: '视频 3.0 720P-图生视频-运镜',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '根据提示词 + 运镜图片生成视频',
|
||||
key: 'jimeng_i2v_recamera_v30',
|
||||
params: [
|
||||
@@ -934,7 +1105,7 @@ export const JimengParams = {
|
||||
// 视频 3.0 1080P-文生视频
|
||||
{
|
||||
name: '视频 3.0 1080P-文生视频',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '视觉表达流畅一致,支持1080P高清渲染',
|
||||
key: 'jimeng_t2v_v30_1080p',
|
||||
params: [
|
||||
@@ -1004,7 +1175,7 @@ export const JimengParams = {
|
||||
// 视频 3.0 1080P-图生视频-首帧
|
||||
{
|
||||
name: '视频 3.0 1080P-图生视频-首帧',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '根据提示词 + 首帧图片生成1080P视频',
|
||||
key: 'jimeng_i2v_first_v30_1080',
|
||||
params: [
|
||||
@@ -1050,7 +1221,7 @@ export const JimengParams = {
|
||||
// 视频 3.0 1080P-图生视频-首尾帧
|
||||
{
|
||||
name: '视频 3.0 1080P-图生视频-首尾帧',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '根据提示词 + 首尾帧图片生成1080P视频',
|
||||
key: 'jimeng_i2v_first_tail_v30_1080',
|
||||
params: [
|
||||
@@ -1097,7 +1268,7 @@ export const JimengParams = {
|
||||
// 视频 3.0Pro 1080P-图生视频
|
||||
{
|
||||
name: '视频 3.0Pro 1080P-图生视频',
|
||||
version: '3.0',
|
||||
icon: { text: '3.0' },
|
||||
label: '根据提示词 + 首帧图片生成1080P视频',
|
||||
key: 'jimeng_ti2v_v30_pro',
|
||||
params: [
|
||||
@@ -1180,8 +1351,122 @@ export const JimengParams = {
|
||||
],
|
||||
},
|
||||
],
|
||||
virtualHuman: [],
|
||||
actionTransfer: [],
|
||||
virtual_human: [
|
||||
{
|
||||
name: '即梦AI数字人',
|
||||
icon: { text: '即梦', size: '!text-base' },
|
||||
label: '即梦同源数字人快速模型,单张图片+音频',
|
||||
key: 'jimeng_realman_avatar_picture_omni_v2',
|
||||
action: 'CVSubmitTask',
|
||||
params: [
|
||||
{
|
||||
name: 'image_urls',
|
||||
label: '人物主体图片',
|
||||
required: true,
|
||||
placeholder: '请上传图片',
|
||||
type: 'image',
|
||||
multiple: false,
|
||||
maxCount: 1,
|
||||
maxSize: 5,
|
||||
accept: '.png,.jpg,.jpeg',
|
||||
info: '建议JPG格式,输入图中为单人、人脸占比大、正面效果较好,其他类型图片效果不佳',
|
||||
},
|
||||
{
|
||||
name: 'audio_url',
|
||||
label: '驱动音频',
|
||||
required: true,
|
||||
placeholder: '请上传音频',
|
||||
type: 'file',
|
||||
multiple: false,
|
||||
maxCount: 1,
|
||||
maxSize: 5,
|
||||
accept: '.mp3,.wav,.m4a',
|
||||
info: '音频建议MP3/WAV格式,时长建议小于15秒以保障生成效果,音频过长可能有效果裂化问题',
|
||||
},
|
||||
{
|
||||
name: 'recognize_key',
|
||||
label: '识别主体请求Key',
|
||||
required: true,
|
||||
value: 'jimeng_realman_avatar_picture_create_role_omni',
|
||||
type: 'hidden',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '火山引擎OmniHuman数字人',
|
||||
icon: { text: '火山', size: '!text-base' },
|
||||
label: '火山引擎OmniHuman数字人模型,单张图片+音频',
|
||||
key: 'realman_avatar_picture_omni_v2',
|
||||
action: 'CVSubmitTask',
|
||||
params: [
|
||||
{
|
||||
name: 'image_urls',
|
||||
label: '人物主体图片',
|
||||
required: true,
|
||||
placeholder: '请上传图片',
|
||||
type: 'image',
|
||||
multiple: false,
|
||||
maxCount: 1,
|
||||
maxSize: 5,
|
||||
accept: '.png,.jpg,.jpeg',
|
||||
info: '建议JPG格式,输入图中为单人、人脸占比大、正面效果较好,其他类型图片效果不佳',
|
||||
},
|
||||
{
|
||||
name: 'audio_url',
|
||||
label: '驱动音频',
|
||||
required: true,
|
||||
placeholder: '请上传音频',
|
||||
type: 'file',
|
||||
multiple: false,
|
||||
maxCount: 1,
|
||||
maxSize: 5,
|
||||
accept: '.mp3,.wav,.m4a',
|
||||
info: '音频建议MP3/WAV格式,时长建议小于15秒以保障生成效果,音频过长可能有效果裂化问题',
|
||||
},
|
||||
{
|
||||
name: 'recognize_key',
|
||||
label: '识别主体请求Key',
|
||||
required: true,
|
||||
value: 'realman_avatar_picture_create_role_omni',
|
||||
type: 'hidden',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
action_transfer: [
|
||||
{
|
||||
name: '即梦AI-动作模仿-4.0',
|
||||
icon: { text: '4.0', fontSize: '!text-xl' },
|
||||
label: '即梦同源的视频动作模仿(生动模式)',
|
||||
key: 'jimeng_dream_actor_m1_gen_video_cv',
|
||||
params: [
|
||||
{
|
||||
name: 'image_urls',
|
||||
label: '人物主体图片',
|
||||
required: true,
|
||||
placeholder: '请上传图片',
|
||||
type: 'image',
|
||||
multiple: false,
|
||||
maxCount: 1,
|
||||
maxSize: 5,
|
||||
accept: '.png,.jpg,.jpeg',
|
||||
info: '图片格式支持 jpeg,jpg,png ,分辨率需在 480x480 以上,1920x1080 以内',
|
||||
},
|
||||
{
|
||||
name: 'video_url',
|
||||
label: '动作视频',
|
||||
required: true,
|
||||
placeholder: '请上传音频',
|
||||
type: 'file',
|
||||
multiple: false,
|
||||
maxCount: 1,
|
||||
maxSize: 100,
|
||||
accept: '.mp4,.mov,.webm',
|
||||
info: '输入的视频时长不可超过30s,支持mp4,mov,webm格式,视频分辨率须在480P以上,2K以内',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const JimengFunctions = [
|
||||
@@ -1196,12 +1481,12 @@ export const JimengFunctions = [
|
||||
name: '视频生成',
|
||||
},
|
||||
{
|
||||
key: 'virtualHuman',
|
||||
key: 'virtual_human',
|
||||
icon: 'icon-shuziren',
|
||||
name: '数字人',
|
||||
},
|
||||
{
|
||||
key: 'actionTransfer',
|
||||
key: 'action_transfer',
|
||||
icon: 'icon-action',
|
||||
name: '动作模仿',
|
||||
},
|
||||
@@ -6,7 +6,7 @@
|
||||
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
import { checkSession } from '@/store/cache'
|
||||
import { JimengFunctions, JimengParams } from '@/store/data/jimeng_data'
|
||||
import { JimengFunctions, JimengParams } from '@/store/data/jimeng_params'
|
||||
import { useSharedStore } from '@/store/sharedata'
|
||||
import { showMessageError, showMessageOK } from '@/utils/dialog'
|
||||
import { httpDownload, httpGet, httpPost } from '@/utils/http'
|
||||
@@ -59,7 +59,6 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
// 切换功能
|
||||
const switchFunction = (f) => {
|
||||
activeFunction.value = f.key
|
||||
formData.value = {}
|
||||
setFunctionPowers()
|
||||
}
|
||||
|
||||
@@ -200,10 +199,14 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
if (formData.value.duration) {
|
||||
formData.value.duration = parseInt(formData.value.duration)
|
||||
}
|
||||
if (formData.value.image_urls && !Array.isArray(formData.value.image_urls)) {
|
||||
formData.value.image_urls = [formData.value.image_urls]
|
||||
|
||||
const data = { ...formData.value }
|
||||
|
||||
if (data.image_urls && !Array.isArray(data.image_urls)) {
|
||||
data.image_urls = [data.image_urls]
|
||||
}
|
||||
const response = await httpPost('/api/jimeng/task', formData.value)
|
||||
|
||||
const response = await httpPost('/api/jimeng/task', data)
|
||||
showMessageOK('任务提交成功')
|
||||
isOver.value = false
|
||||
await fetchData(1)
|
||||
@@ -279,12 +282,6 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 播放视频
|
||||
const playVideo = (item) => {
|
||||
currentVideoUrl.value = item.video_url
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const setFunctionPowers = () => {
|
||||
if (activeFunction.value === 'image') {
|
||||
currentPowerCost.value = `${powerConfig.image}积分/张`
|
||||
@@ -333,7 +330,6 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
isLogin,
|
||||
showDialog,
|
||||
currentVideoUrl,
|
||||
|
||||
// 配置
|
||||
functions,
|
||||
activeFunction,
|
||||
@@ -355,7 +351,6 @@ export const useJimengStore = defineStore('jimeng', () => {
|
||||
downloadFile,
|
||||
retryTask,
|
||||
removeJob,
|
||||
playVideo,
|
||||
cleanup,
|
||||
|
||||
// 工具函数
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
<div class="video-mask" @click="store.playVideo(item)">
|
||||
<div class="video-mask" @click="playVideo(item)">
|
||||
<div class="play-btn">
|
||||
<img src="/images/play.svg" alt="播放" />
|
||||
</div>
|
||||
@@ -370,15 +370,15 @@
|
||||
</div>
|
||||
|
||||
<!-- 视频预览对话框 -->
|
||||
<el-dialog v-model="store.showDialog" title="视频预览" center>
|
||||
<el-dialog v-model="store.showDialog" title="视频预览" @close="stopVideoPlay" center>
|
||||
<div class="flex justify-center items-center">
|
||||
<video
|
||||
ref="videoPreviewRef"
|
||||
:src="store.currentVideoUrl"
|
||||
autoplay
|
||||
controls
|
||||
preload="auto"
|
||||
loop
|
||||
muted
|
||||
style="max-height: calc(100vh - 100px); max-width: 100vw; object-fit: cover"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
@@ -414,6 +414,23 @@ const templatePreview = ref('')
|
||||
// 新增:提示词指南折叠面板状态(默认收起)
|
||||
const guideActive = ref([])
|
||||
|
||||
const videoPreviewRef = ref(null)
|
||||
// 播放视频
|
||||
const playVideo = (item) => {
|
||||
store.currentVideoUrl = item.video_url
|
||||
store.showDialog = true
|
||||
if (videoPreviewRef.value) {
|
||||
videoPreviewRef.value.play()
|
||||
}
|
||||
}
|
||||
|
||||
// 停止视频播放
|
||||
const stopVideoPlay = () => {
|
||||
if (videoPreviewRef.value) {
|
||||
videoPreviewRef.value.pause()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
store.init()
|
||||
})
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<script setup>
|
||||
import ParamBuilder from '@/components/ParamBuilder.vue'
|
||||
import { JimengFunctions, JimengParams } from '@/store/data/jimeng_data'
|
||||
import { JimengFunctions, JimengParams } from '@/store/data/jimeng_params'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const functions = JimengFunctions
|
||||
|
||||
Reference in New Issue
Block a user