Jimeng AI 4.0 for mobile is ready

This commit is contained in:
GeekMaster
2025-09-16 19:13:41 +08:00
parent 49254b2a32
commit 809d8d71bd
5 changed files with 506 additions and 1251 deletions

View File

@@ -5,7 +5,7 @@
- Bug 修复:修复超级管理员无法修改密码的 Bug
- Bug 修复:微信登录配置更新后,没有同步更新到系统配置
- 功能优化: 给 AI 对话 API 加上线程锁,确保同一个用户同时只有一个对话请求
- 功能新增:支持即梦 AI 4.0 图片编辑,即梦 AI 数字人,动作迁移功能
- 功能新增:支持即梦 AI 4.0 图片编辑,即梦 AI 数字人,动作迁移功能。🔥🔥🔥
## v4.2.6

View File

@@ -887,3 +887,284 @@
transform: rotate(360deg);
}
}
/* Dark 主题样式 - 按照 theme-dark.scss 的模式 */
:root[data-theme='dark'] .jimeng-create {
background-color: rgb(13, 20, 53);
/* 页面头部样式 */
.sticky {
background-color: rgb(31, 41, 55) !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
h1 {
color: rgb(255, 255, 255) !important;
}
.iconfont {
color: rgb(156, 163, 175) !important;
}
button:hover {
background-color: rgb(75, 85, 99) !important;
}
}
/* 功能分类选择 */
.jimeng-create__content {
.bg-white {
background-color: rgb(55, 65, 81) !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.text-gray-700 {
color: rgb(209, 213, 219) !important;
}
.text-gray-900 {
color: rgb(255, 255, 255) !important;
}
.text-gray-600 {
color: rgb(156, 163, 175) !important;
}
.text-gray-500 {
color: rgb(156, 163, 175) !important;
}
.bg-gray-100:hover {
background-color: rgb(75, 85, 99) !important;
}
/* Element Plus 组件样式覆盖 */
:deep(.el-input__wrapper) {
background-color: rgb(31, 41, 55) !important;
border-color: rgb(75, 85, 99) !important;
box-shadow: none !important;
}
:deep(.el-input__inner) {
color: rgb(209, 213, 219) !important;
background-color: transparent !important;
}
:deep(.el-input__inner::placeholder) {
color: rgb(156, 163, 175) !important;
}
:deep(.el-textarea__inner) {
color: rgb(209, 213, 219) !important;
background-color: transparent !important;
}
:deep(.el-textarea__inner::placeholder) {
color: rgb(156, 163, 175) !important;
}
:deep(.el-switch__core) {
background-color: rgb(75, 85, 99) !important;
border-color: rgb(75, 85, 99) !important;
}
:deep(.el-switch.is-checked .el-switch__core) {
background-color: rgb(139, 92, 246) !important;
border-color: rgb(139, 92, 246) !important;
}
:deep(.el-slider__runway) {
background-color: rgb(75, 85, 99) !important;
}
:deep(.el-slider__bar) {
background-color: rgb(139, 92, 246) !important;
}
:deep(.el-slider__button) {
border-color: rgb(139, 92, 246) !important;
}
:deep(.el-tooltip__trigger) {
color: rgb(156, 163, 175) !important;
}
}
/* 提交按钮 */
.bg-gradient-to-r {
background: linear-gradient(88deg, #af61f0 1.44%, #5b62ce) !important;
&:hover {
background: linear-gradient(88deg, #9f51e0 1.44%, #4b52be) !important;
}
&:disabled {
background: linear-gradient(88deg, #6b7280 1.44%, #4b5563) !important;
}
}
/* 作品列表 */
.jimeng-create__works {
&-title {
color: rgb(255, 255, 255) !important;
}
&-item {
background-color: rgb(55, 65, 81) !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
&-content {
.jimeng-create__works-item-info {
&-title {
color: rgb(255, 255, 255) !important;
}
&-prompt {
color: rgb(209, 213, 219) !important;
}
&-tags {
&-item {
background-color: rgb(75, 85, 99) !important;
color: rgb(209, 213, 219) !important;
&--warning {
background-color: rgb(239, 68, 68) !important;
color: rgb(255, 255, 255) !important;
}
&--primary {
background-color: rgb(59, 130, 246) !important;
color: rgb(255, 255, 255) !important;
}
&--power {
background-color: rgb(139, 92, 246) !important;
color: rgb(255, 255, 255) !important;
}
}
}
}
}
&-quick-actions {
button {
color: rgb(156, 163, 175) !important;
&:hover {
color: rgb(209, 213, 219) !important;
}
}
}
&-error {
&-content {
background-color: rgb(31, 41, 55) !important;
border-color: rgb(239, 68, 68) !important;
.jimeng-create__works-item-error-text {
color: rgb(239, 68, 68) !important;
}
.jimeng-create__works-item-error-copy-btn {
color: rgb(156, 163, 175) !important;
&:hover {
color: rgb(209, 213, 219) !important;
}
}
}
}
}
&-loading {
color: rgb(156, 163, 175) !important;
}
&-finished {
color: rgb(156, 163, 175) !important;
}
}
/* 媒体预览弹窗 */
.jimeng-create__media-dialog {
background-color: rgba(0, 0, 0, 0.8) !important;
&-content {
background-color: rgb(55, 65, 81) !important;
box-shadow: 0 0 15px rgba(107, 80, 225, 0.8) !important;
}
&-header {
background-color: rgb(31, 41, 55) !important;
border-bottom-color: rgb(75, 85, 99) !important;
h3 {
color: rgb(255, 255, 255) !important;
}
button {
color: rgb(156, 163, 175) !important;
&:hover {
color: rgb(209, 213, 219) !important;
}
}
}
}
/* 图片上传组件 */
:deep(.image-upload) {
.upload-area {
background-color: rgb(31, 41, 55) !important;
border-color: rgb(75, 85, 99) !important;
&:hover {
border-color: rgb(139, 92, 246) !important;
background-color: rgb(55, 65, 81) !important;
}
}
.upload-text {
color: rgb(209, 213, 219) !important;
}
.upload-icon {
color: rgb(139, 92, 246) !important;
}
}
/* 自定义选择组件 */
:deep(.custom-select) {
.select-trigger {
background-color: rgb(31, 41, 55) !important;
border-color: rgb(75, 85, 99) !important;
color: rgb(209, 213, 219) !important;
}
.select-dropdown {
background-color: rgb(55, 65, 81) !important;
border-color: rgb(75, 85, 99) !important;
box-shadow: 0 0 15px rgba(107, 80, 225, 0.8) !important;
}
.select-option {
color: rgb(209, 213, 219) !important;
&:hover {
background-color: rgb(75, 85, 99) !important;
}
&.selected {
background-color: rgb(139, 92, 246) !important;
color: rgb(255, 255, 255) !important;
}
}
}
/* 空状态组件 */
:deep(.van-empty) {
.van-empty__description {
color: rgb(156, 163, 175) !important;
}
}
}

View File

@@ -272,6 +272,7 @@ export const useJimengStore = defineStore('jimeng', () => {
const response = await httpGet('/api/jimeng/remove', { id: item.id })
if (response.data) {
showMessageOK('删除成功')
isOver.value = false
await fetchData(1)
}
} catch (error) {
@@ -346,6 +347,7 @@ export const useJimengStore = defineStore('jimeng', () => {
getTaskStatusText,
getTaskType,
switchTaskFilter,
setFunctionPowers,
fetchData,
submitTask,
downloadFile,

View File

@@ -1,525 +0,0 @@
import { showMessageError, showMessageOK } from '@/utils/dialog'
import { httpDownload, httpGet, httpPost } from '@/utils/http'
import { replaceImg } from '@/utils/libs'
import { defineStore } from 'pinia'
import { showConfirmDialog } from 'vant'
import { computed, reactive, ref, watch } from 'vue'
export const useJimengStore = defineStore('mobile-jimeng', () => {
// 响应式数据
const activeCategory = ref('image_generation')
const useImageInput = ref(false)
const submitting = ref(false)
const listLoading = ref(false)
const listFinished = ref(false)
const currentList = ref([])
const showMediaDialog = ref(false)
const currentMediaUrl = ref('')
const currentPrompt = ref('')
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const currentPowerCost = ref(0)
const taskPulling = ref(true)
const tastPullHandler = ref(null)
// 新增:算力配置
const powerConfig = ref({
text_to_image: 20,
image_to_image: 30,
image_edit: 25,
image_effects: 15,
text_to_video: 100,
image_to_video: 120,
})
// 功能分类
const categories = ref([
{ key: 'image_generation', name: '图像生成' },
{ key: 'image_editing', name: '图像编辑' },
{ key: 'image_effects', name: '图像特效' },
{ key: 'video_generation', name: '视频生成' },
])
// 选项数据
const imageSizeOptions = [
{ label: '1:1 (1328x1328)', value: '1328x1328' },
{ label: '3:2 (1584x1056)', value: '1584x1056' },
{ label: '2:3 (1056x1584)', value: '1056x1584' },
{ label: '4:3 (1472x1104)', value: '1472x1104' },
{ label: '3:4 (1104x1472)', value: '1104x1472' },
{ label: '16:9 (1664x936)', value: '1664x936' },
{ label: '9:16 (936x1664)', value: '936x1664' },
{ label: '21:9 (2016x864)', value: '2016x864' },
{ label: '9:21 (864x2016)', value: '864x2016' },
]
const videoAspectRatioOptions = [
{ label: '1:1 (正方形)', value: '1:1' },
{ label: '16:9 (横版)', value: '16:9' },
{ label: '9:16 (竖版)', value: '9:16' },
]
const imageEffectsTemplateOptions = [
{
label: '毛毡3D拍立得风格',
value: 'felt_3d_polaroid',
preview: '/images/jimeng/templates/felt_3d_polaroid.png',
},
{ label: '像素世界风', value: 'my_world', preview: '/images/jimeng/templates/my_world.png' },
{
label: '像素世界-万物通用版',
value: 'my_world_universal',
preview: '/images/jimeng/templates/my_world_universal.png',
},
{
label: '盲盒玩偶风',
value: 'plastic_bubble_figure',
preview: '/images/jimeng/templates/plastic_bubble_figure.png',
},
{
label: '塑料泡罩人偶-文字卡头版',
value: 'plastic_bubble_figure_cartoon_text',
preview: '/images/jimeng/templates/plastic_bubble_figure_cartoon_text.png',
},
{
label: '毛绒玩偶风',
value: 'furry_dream_doll',
preview: '/images/jimeng/templates/furry_dream_doll.png',
},
{
label: '迷你世界玩偶风',
value: 'micro_landscape_mini_world',
preview: '/images/jimeng/templates/micro_landscape_mini_world.png',
},
{
label: '微型景观小世界-职业版',
value: 'micro_landscape_mini_world_professional',
preview: '/images/jimeng/templates/micro_landscape_mini_world_professional.png',
},
{
label: '亚克力挂饰',
value: 'acrylic_ornaments',
preview: '/images/jimeng/templates/acrylic_ornaments.png',
},
{
label: '毛毡钥匙扣',
value: 'felt_keychain',
preview: '/images/jimeng/templates/felt_keychain.png',
},
{
label: 'Lofi 像素人物小卡',
value: 'lofi_pixel_character_mini_card',
preview: '/images/jimeng/templates/lofi_pixel_character_mini_card.png',
},
{
label: '天使形象手办',
value: 'angel_figurine',
preview: '/images/jimeng/templates/angel_figurine.png',
},
{
label: '躺在毛茸茸肚皮里',
value: 'lying_in_fluffy_belly',
preview: '/images/jimeng/templates/lying_in_fluffy_belly.png',
},
{ label: '玻璃球', value: 'glass_ball', preview: '/images/jimeng/templates/glass_ball.png' },
]
// 功能参数
// 各功能的参数
const textToImageParams = reactive({
size: '1328x1328',
scale: 2.5,
seed: -1,
use_pre_llm: true,
})
const imageToImageParams = reactive({
image_input: '',
size: '1328x1328',
gpen: 0.4,
skin: 0.3,
skin_unifi: 0,
gen_mode: 'creative',
seed: -1,
})
const imageEditParams = reactive({
image_input: '',
scale: 0.5,
seed: -1,
})
const imageEffectsParams = reactive({
image_input: '',
template_id: '',
size: '1328x1328',
})
const textToVideoParams = reactive({
aspect_ratio: '16:9',
seed: -1,
})
const imageToVideoParams = reactive({
image_urls: [],
aspect_ratio: '16:9',
seed: -1,
})
// 计算属性
const activeFunction = computed(() => {
if (activeCategory.value === 'image_generation') {
return useImageInput.value ? 'image_to_image' : 'text_to_image'
} else if (activeCategory.value === 'image_editing') {
return 'image_edit'
} else if (activeCategory.value === 'image_effects') {
return 'image_effects'
} else if (activeCategory.value === 'video_generation') {
return useImageInput.value ? 'image_to_video' : 'text_to_video'
}
return 'text_to_image'
})
// 新增:动态计算当前算力消耗
const updateCurrentPowerCost = () => {
const functionKey = activeFunction.value
currentPowerCost.value = powerConfig.value[functionKey] || 10
}
// 监听任务类型变化,自动更新算力
watch(
[activeCategory, useImageInput],
() => {
updateCurrentPowerCost()
},
{ immediate: true }
)
// Actions
const getCategoryIcon = (category) => {
const iconMap = {
image_generation: 'iconfont icon-image',
image_editing: 'iconfont icon-edit',
image_effects: 'iconfont icon-chuangzuo',
video_generation: 'iconfont icon-video',
}
return iconMap[category] || 'iconfont icon-image'
}
const switchCategory = (key) => {
activeCategory.value = key
useImageInput.value = false
}
// 新增:获取算力配置
const fetchPowerConfig = async () => {
try {
const res = await httpGet('/api/jimeng/power-config')
if (res.data) {
powerConfig.value = res.data
updateCurrentPowerCost() // 更新当前算力消耗
}
} catch (error) {
console.error('获取算力配置失败:', error)
}
}
const submitTask = () => {
if (!currentPrompt.value.trim()) {
showMessageError('请输入提示词')
return
}
submitting.value = true
let requestData = { task_type: activeFunction.value, prompt: currentPrompt.value }
// 根据功能类型添加相应参数
switch (activeFunction.value) {
case 'text_to_image':
Object.assign(requestData, {
width: parseInt(textToImageParams.size.split('x')[0]),
height: parseInt(textToImageParams.size.split('x')[1]),
scale: textToImageParams.scale,
seed: textToImageParams.seed,
use_pre_llm: textToImageParams.use_pre_llm,
})
break
case 'image_to_image':
Object.assign(requestData, {
image_input: imageToImageParams.image_input[0],
width: parseInt(imageToImageParams.size.split('x')[0]),
height: parseInt(imageToImageParams.size.split('x')[1]),
gpen: imageToImageParams.gpen,
skin: imageToImageParams.skin,
skin_unifi: imageToImageParams.skin_unifi,
gen_mode: imageToImageParams.gen_mode,
seed: imageToImageParams.seed,
})
break
case 'image_edit':
Object.assign(requestData, {
image_input: imageEditParams.image_input[0],
scale: imageEditParams.scale,
seed: imageEditParams.seed,
})
break
case 'image_effects':
Object.assign(requestData, {
image_input: imageEffectsParams.image_input[0],
template_id: imageEffectsParams.template_id,
width: parseInt(imageEffectsParams.size.split('x')[0]),
height: parseInt(imageEffectsParams.size.split('x')[1]),
prompt: imageEffectsParams.prompt,
})
break
case 'text_to_video':
Object.assign(requestData, {
aspect_ratio: textToVideoParams.aspect_ratio,
seed: textToVideoParams.seed,
})
break
case 'image_to_video':
Object.assign(requestData, {
image_urls: imageToVideoParams.image_input,
aspect_ratio: imageToVideoParams.aspect_ratio,
seed: imageToVideoParams.seed,
})
break
}
return httpPost('/api/jimeng/task', requestData)
.then(() => {
fetchData(1)
taskPulling.value = true
showMessageOK('创建任务成功')
currentPrompt.value = ''
})
.catch((e) => {
showMessageError('创建任务失败:' + e.message)
})
.finally(() => {
submitting.value = false
})
}
const fetchData = (_page) => {
if (_page) {
page.value = _page
}
listLoading.value = true
return httpPost('/api/jimeng/jobs', { page: page.value, page_size: pageSize.value })
.then((res) => {
total.value = res.data.total
let needPull = false
const items = []
if (res.data.items) {
for (let v of res.data.items) {
if (v.status === 'in_queue' || v.status === 'generating') {
needPull = true
}
items.push(v)
}
}
listLoading.value = false
taskPulling.value = needPull
if (page.value === 1) {
currentList.value = items
} else {
currentList.value.push(...items)
}
if (items.length < pageSize.value) {
listFinished.value = true
}
})
.catch((e) => {
listLoading.value = false
showMessageError('获取作品列表失败:' + e.message)
})
}
const loadMore = () => {
page.value++
fetchData()
}
const playMedia = (item) => {
currentMediaUrl.value = item.img_url || item.video_url
showMediaDialog.value = true
}
const downloadFile = async (item) => {
const url = replaceImg(item.video_url || item.img_url)
const downloadURL = `/api/download?url=${url}`
const urlObj = new URL(url)
const fileName = urlObj.pathname.split('/').pop()
item.downloading = true
try {
const response = await httpDownload(downloadURL)
const blob = new Blob([response.data])
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(link.href)
item.downloading = false
} catch (error) {
showMessageError('下载失败')
item.downloading = false
}
}
const retryTask = (id) => {
return httpGet('/api/jimeng/retry', { id })
.then(() => {
showMessageOK('重试任务成功')
fetchData(1)
})
.catch((e) => {
showMessageError('重试任务失败:' + e.message)
})
}
const removeJob = async (item) => {
return showConfirmDialog({
title: '确认删除',
message: '此操作将会删除任务相关文件,继续操作吗?',
confirmButtonText: '确认删除',
cancelButtonText: '取消',
})
.then(() => {
return httpGet('/api/jimeng/remove', { id: item.id })
.then(() => {
showMessageOK('任务删除成功')
fetchData(1)
})
.catch((e) => {
showMessageError('任务删除失败:' + e.message)
})
})
.catch(() => {})
}
const getFunctionName = (type) => {
const nameMap = {
text_to_image: '文生图',
image_to_image: '图生图',
image_edit: '图像编辑',
image_effects: '图像特效',
text_to_video: '文生视频',
image_to_video: '图生视频',
}
return nameMap[type] || type
}
const getTaskType = (type) => {
return type.includes('video') ? 'warning' : 'primary'
}
const startTaskPolling = () => {
tastPullHandler.value = setInterval(() => {
if (taskPulling.value) {
fetchData(1)
}
}, 5000)
}
const stopTaskPolling = () => {
if (tastPullHandler.value) {
clearInterval(tastPullHandler.value)
}
}
const closeMediaDialog = () => {
showMediaDialog.value = false
currentMediaUrl.value = ''
}
// 新增:复制提示词功能
const copyPrompt = (prompt) => {
navigator.clipboard
.writeText(prompt)
.then(() => {
showMessageOK('提示词已复制')
})
.catch(() => {
showMessageError('复制失败')
})
}
// 新增:复制错误信息功能
const copyErrorMsg = (msg) => {
navigator.clipboard
.writeText(msg)
.then(() => {
showMessageOK('错误信息已复制')
})
.catch(() => {
showMessageError('复制失败')
})
}
// 新增:初始化方法
const init = async () => {
await fetchPowerConfig()
}
return {
// State
activeCategory,
useImageInput,
submitting,
listLoading,
listFinished,
currentList,
showMediaDialog,
currentMediaUrl,
currentPrompt,
page,
pageSize,
total,
currentPowerCost,
taskPulling,
tastPullHandler,
categories,
imageSizeOptions,
videoAspectRatioOptions,
imageEffectsTemplateOptions,
textToImageParams,
imageToImageParams,
imageEditParams,
imageEffectsParams,
textToVideoParams,
imageToVideoParams,
powerConfig,
// Computed
activeFunction,
// Actions
getCategoryIcon,
switchCategory,
submitTask,
fetchData,
loadMore,
playMedia,
downloadFile,
retryTask,
removeJob,
getFunctionName,
getTaskType,
startTaskPolling,
stopTaskPolling,
closeMediaDialog,
fetchPowerConfig,
copyPrompt,
copyErrorMsg,
init,
}
})

View File

@@ -14,252 +14,45 @@
</div>
</div>
<!-- 功能分类选择 -->
<!-- 功能与参数复用 PC 端逻辑 -->
<div class="jimeng-create__content">
<CustomTabs
v-model="jimengStore.activeCategory"
@update:modelValue="jimengStore.switchCategory"
>
<CustomTabPane
:label="jimengStore.categories[0].name"
:name="jimengStore.categories[0].key"
>
<!-- 功能分类按钮来源于 PC store.functions -->
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<CustomTabs v-model="store.activeFunction" @tab-click="store.setFunctionPowers">
<CustomTabPane v-for="f in store.functions" :key="f.key" :name="f.key" :label="f.name">
<template #label>
<span>{{ jimengStore.categories[0].name }}</span>
<i class="iconfont mr-1" :class="f.icon"></i>
{{ f.name }}
</template>
<!-- 参数容器 -->
<div class="py-3">
<!-- 文生图 -->
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="mb-3">
<label class="text-gray-700 font-semibold">提示词</label>
</div>
<el-input
v-model="jimengStore.currentPrompt"
type="textarea"
placeholder="请输入图片描述,越详细越好"
:rows="4"
maxlength="2000"
show-word-limit
/>
</div>
<!-- 功能开关 -->
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="flex justify-between items-center w-full">
<span class="text-gray-700 font-semibold">图生图人像写真</span>
<el-switch v-model="jimengStore.useImageInput" size="default" />
</div>
</div>
<!-- 图生图参数 -->
<div class="bg-white rounded-xl p-4 shadow-sm mb-3" v-if="jimengStore.useImageInput">
<ImageUpload
v-model="jimengStore.imageToImageParams.image_input"
:max-count="1"
:multiple="false"
/>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<label class="block text-gray-700 mb-3 font-semibold">图片尺寸</label>
<CustomSelect
v-model="jimengStore.textToImageParams.size"
:options="jimengStore.imageSizeOptions"
title="选择尺寸"
/>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<span class="flex justify-between items-center mb-3">
<span class="text-gray-700 font-semibold">创意度</span>
<el-tooltip content="创意度越高,影响文本描述的程度越高" placement="top">
<i class="iconfont icon-info cursor-pointer ml-1"></i>
</el-tooltip>
</span>
<div class="mt-3">
<el-slider
v-model="jimengStore.textToImageParams.scale"
:min="1"
:max="10"
:step="0.5"
/>
</div>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="flex justify-between items-center w-full">
<label class="text-gray-700 font-semibold">智能优化提示词</label>
<el-switch v-model="jimengStore.textToImageParams.use_pre_llm" size="default" />
</div>
</div>
</div>
</CustomTabPane>
<CustomTabPane
:name="jimengStore.categories[1].key"
:label="jimengStore.categories[1].name"
>
<template #label>
<span>{{ jimengStore.categories[1].name }}</span>
</template>
<div class="py-3">
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="mb-3">
<label class="text-gray-700 font-semibold">编辑提示词</label>
</div>
<el-input
v-model="jimengStore.currentPrompt"
type="textarea"
placeholder="描述你想要的编辑效果"
:rows="4"
maxlength="2000"
show-word-limit
/>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<ImageUpload
v-model="jimengStore.imageEditParams.image_input"
:max-count="1"
:multiple="true"
/>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="mb-3">
<label class="text-gray-700 font-semibold">编辑强度</label>
</div>
<el-slider
v-model="jimengStore.imageEditParams.scale"
:min="0"
:max="1"
:step="0.1"
/>
</div>
</div>
</CustomTabPane>
<CustomTabPane
:name="jimengStore.categories[2].key"
:label="jimengStore.categories[2].name"
>
<template #label>
<span>{{ jimengStore.categories[2].name }}</span>
</template>
<div class="py-3">
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<ImageUpload
v-model="jimengStore.imageEffectsParams.image_input"
:max-count="1"
:multiple="true"
/>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="mb-3">
<label class="text-gray-700 font-semibold">特效模板</label>
</div>
<CustomSelect
v-model="jimengStore.imageEffectsParams.template_id"
:options="jimengStore.imageEffectsTemplateOptions"
title="选择模板"
>
<template #option="{ option, selected }">
<div class="flex items-center w-full">
<el-image :src="option.preview" fit="cover" class="w-10 h-10 rounded-lg mr-2" />
<span
class="font-bold text-gray-900 mr-2"
:class="{ '!text-purple-600': selected }"
>{{ option.label }}</span
>
</div>
</template>
</CustomSelect>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="mb-3">
<label class="text-gray-700 font-semibold">输出尺寸</label>
</div>
<CustomSelect
v-model="jimengStore.imageEffectsParams.size"
:options="jimengStore.imageSizeOptions"
title="选择尺寸"
/>
</div>
</div>
</CustomTabPane>
<CustomTabPane
:name="jimengStore.categories[3].key"
:label="jimengStore.categories[3].name"
>
<template #label>
<span>{{ jimengStore.categories[3].name }}</span>
</template>
<div class="py-3">
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="mb-3">
<label class="text-gray-700 font-semibold">提示词</label>
</div>
<el-input
v-model="jimengStore.currentPrompt"
type="textarea"
placeholder="请输入你想要的视频效果"
:rows="4"
maxlength="2000"
show-word-limit
/>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="flex justify-between items-center w-full">
<label class="text-gray-700 font-semibold">使用图片辅助生成</label>
<el-switch v-model="jimengStore.useImageInput" size="default" />
</div>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm mb-3" v-if="jimengStore.useImageInput">
<ImageUpload
v-model="jimengStore.imageToVideoParams.image_input"
:max-count="2"
:multiple="true"
/>
</div>
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div class="mb-3">
<label class="text-gray-700 font-semibold">视频比例</label>
</div>
<CustomSelect
v-model="jimengStore.textToVideoParams.aspect_ratio"
:options="jimengStore.videoAspectRatioOptions"
title="选择比例"
/>
</div>
</div>
</CustomTabPane>
</CustomTabs>
</div>
<!-- 参数构建器移动端组件 -->
<div class="mb-3">
<ParamBuilderMobile
v-model="store.formData"
:required-keys="store.requiredKeys"
@update:required-keys="(v) => (store.requiredKeys = v)"
:items="store.functionParams[store.activeFunction]"
:progress="store.progress[store.activeFunction]"
/>
</div>
<!-- 提交按钮 -->
<div class="bg-white rounded-xl p-4 shadow-sm mb-3">
<div
class="bg-white rounded-xl p-4 shadow-sm mb-3"
v-if="store.functionParams[store.activeFunction].length > 0"
>
<button
class="w-full py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-xl disabled:from-gray-400 disabled:to-gray-400 disabled:cursor-not-allowed hover:from-blue-600 hover:to-purple-700 transition-all duration-200 flex items-center justify-center space-x-2 text-base"
type="button"
@click="jimengStore.submitTask"
:disabled="jimengStore.submitting"
@click="store.submitTask"
:disabled="store.submitting"
>
<i v-if="jimengStore.submitting" class="iconfont icon-loading animate-spin"></i>
<i v-if="store.submitting" class="iconfont icon-loading animate-spin"></i>
<i v-else class="iconfont icon-chuangzuo"></i>
<span>{{
jimengStore.submitting ? '创作中...' : `立即生成 (${jimengStore.currentPowerCost}算力)`
}}</span>
<span>立即生成 ({{ store.currentPowerCost }})</span>
</button>
</div>
</div>
@@ -267,11 +60,18 @@
<!-- 作品列表 -->
<div class="jimeng-create__works">
<h2 class="jimeng-create__works-title">我的作品</h2>
<div class="jimeng-create__works-list space-y-4" v-if="jimengStore.currentList.length > 0">
<van-list
:loading="store.loading"
@update:loading="store.loading = $event"
:finished="store.isOver"
finished-text="没有更多了"
@load="onLoadMore"
>
<div class="flex flex-col space-y-4">
<div
v-for="item in jimengStore.currentList"
v-for="item in store.currentList"
:key="item.id"
class="jimeng-create__works-item"
class="jimeng-create__works-item w-full"
>
<div class="jimeng-create__works-item-content">
<div class="jimeng-create__works-item-thumb">
@@ -304,7 +104,7 @@
</video>
<div
class="video-mask absolute top-0 left-0 w-full h-full flex justify-center items-center"
@click="jimengStore.playMedia(item)"
@click="playMedia(item)"
>
<div class="play-btn">
<img src="/images/play.svg" alt="播放" />
@@ -332,14 +132,17 @@
<div class="jimeng-create__works-item-info-header">
<div class="flex-1">
<h3 class="jimeng-create__works-item-info-title">
{{ jimengStore.getFunctionName(item.type) }}
{{ store.getFunctionName(item.type) }}
</h3>
<p class="jimeng-create__works-item-info-prompt line-clamp-2">
{{ item.prompt }}
</p>
</div>
<!-- 任务状态 -->
<div v-if="item.status !== 'success'" class="jimeng-create__works-item-info-status">
<div
v-if="item.status !== 'success'"
class="jimeng-create__works-item-info-status"
>
<div
v-if="item.status === 'failed'"
class="jimeng-create__works-item-info-status--failed"
@@ -361,12 +164,12 @@
<span
:class="[
'jimeng-create__works-item-info-tags-item',
jimengStore.getTaskType(item.type) === 'warning'
store.getTaskType(item.type) === 'warning'
? 'jimeng-create__works-item-info-tags-item--warning'
: 'jimeng-create__works-item-info-tags-item--primary',
]"
>
{{ jimengStore.getFunctionName(item.type) }}
{{ store.getFunctionName(item.type) }}
</span>
<span
v-if="item.power"
@@ -384,7 +187,7 @@
<!-- 复制提示词 -->
<button
v-if="item.prompt"
@click="jimengStore.copyPrompt(item.prompt)"
@click="store.copyPrompt(item.prompt)"
class="jimeng-create__works-item-quick-action-btn"
title="复制提示词"
>
@@ -394,7 +197,7 @@
<!-- 下载 -->
<button
v-if="item.status === 'success' && (item.img_url || item.video_url)"
@click="jimengStore.downloadFile(item)"
@click="store.downloadFile(item)"
:disabled="item.downloading"
class="p-2 text-blue-500"
>
@@ -407,7 +210,7 @@
<!-- 重试 -->
<button
v-if="item.status === 'failed'"
@click="jimengStore.retryTask(item.id)"
@click="store.retryTask(item.id)"
class="p-2 text-green-500"
>
<i class="iconfont icon-refresh"></i>
@@ -415,7 +218,7 @@
</button>
<!-- 删除 -->
<button @click="jimengStore.removeJob(item)" class="p-2 text-red-500">
<button @click="store.removeJob(item)" class="p-2 text-red-500">
<i class="iconfont icon-remove"></i>
<span class="ml-1">删除</span>
</button>
@@ -431,7 +234,7 @@
item.err_msg
}}</span>
<button
@click="jimengStore.copyErrorMsg(item.err_msg)"
@click="store.copyErrorMsg(item.err_msg)"
class="jimeng-create__works-item-error-copy-btn"
title="复制错误信息"
>
@@ -440,42 +243,26 @@
</div>
</div>
</div>
<!-- 加载更多 -->
<div v-if="jimengStore.listLoading" class="jimeng-create__works-loading">
<i class="iconfont icon-loading animate-spin"></i>
</div>
</van-list>
<!-- 没有更多了 -->
<div
v-if="jimengStore.listFinished && !jimengStore.listLoading"
class="jimeng-create__works-finished"
>
没有更多了
</div>
</div>
<div class="px-4" v-else>
<div class="px-4" v-if="store.currentList.length === 0 && !store.loading">
<van-empty description="暂无数据" image-size="120" />
</div>
</div>
<!-- 媒体预览弹窗 -->
<div
v-if="jimengStore.showMediaDialog"
class="jimeng-create__media-dialog"
@click="jimengStore.closeMediaDialog"
>
<div v-if="store.showDialog" class="jimeng-create__media-dialog" @click="closeMediaDialog">
<div @click.stop class="jimeng-create__media-dialog-content animate-scale-up">
<div class="jimeng-create__media-dialog-header">
<h3>媒体预览</h3>
<button @click="jimengStore.closeMediaDialog">
<button @click="closeMediaDialog">
<i class="iconfont icon-error"></i>
</button>
</div>
<div class="jimeng-create__media-dialog-body">
<video
:src="jimengStore.currentMediaUrl"
:src="store.currentVideoUrl"
controls
autoplay
class="w-full max-h-[60vh] rounded-lg object-cover"
@@ -489,335 +276,45 @@
</template>
<script setup>
import ImageUpload from '@/components/ImageUpload.vue'
import CustomSelect from '@/components/mobile/CustomSelect.vue'
import ParamBuilderMobile from '@/components/mobile/ParamBuilderMobile.vue'
import CustomTabPane from '@/components/ui/CustomTabPane.vue'
import CustomTabs from '@/components/ui/CustomTabs.vue'
import { checkSession } from '@/store/cache'
import { useJimengStore } from '@/store/mobile/jimeng'
import { onMounted, onUnmounted, ref } from 'vue'
import { useJimengStore } from '@/store/jimeng'
import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const jimengStore = useJimengStore()
const store = useJimengStore()
// 模板预览相关
const templatePreview = ref('')
// 处理模板变更
const handleTemplateChange = (value) => {
const selectedTemplate = jimengStore.imageEffectsTemplateOptions.find(
(opt) => opt.value === value
)
if (selectedTemplate) {
templatePreview.value = selectedTemplate.preview || ''
// 自动设置提示词为模板名称
jimengStore.currentPrompt = selectedTemplate.label
}
function goBack() {
router.back()
}
function playMedia(item) {
store.currentVideoUrl = item.video_url
store.showDialog = true
}
function closeMediaDialog() {
store.showDialog = false
store.currentVideoUrl = ''
}
// 生命周期
onMounted(() => {
checkSession()
.then(() => {
jimengStore.init() // 初始化算力配置
jimengStore.fetchData(1)
jimengStore.startTaskPolling()
})
.catch(() => {})
store.init()
})
onUnmounted(() => {
jimengStore.stopTaskPolling()
if (store.cleanup) {
store.cleanup()
}
})
// 工具方法
const goBack = () => {
router.back()
function onLoadMore() {
store.fetchData(store.page + 1)
}
</script>
<style lang="scss" scoped>
@use '@/assets/css/mobile/jimeng.scss';
/* Dark 主题样式 - 按照 theme-dark.scss 的模式 */
:root[data-theme='dark'] .jimeng-create {
background-color: rgb(13, 20, 53);
/* 页面头部样式 */
.sticky {
background-color: rgb(31, 41, 55) !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
h1 {
color: rgb(255, 255, 255) !important;
}
.iconfont {
color: rgb(156, 163, 175) !important;
}
button:hover {
background-color: rgb(75, 85, 99) !important;
}
}
/* 功能分类选择 */
.jimeng-create__content {
.bg-white {
background-color: rgb(55, 65, 81) !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.text-gray-700 {
color: rgb(209, 213, 219) !important;
}
.text-gray-900 {
color: rgb(255, 255, 255) !important;
}
.text-gray-600 {
color: rgb(156, 163, 175) !important;
}
.text-gray-500 {
color: rgb(156, 163, 175) !important;
}
.bg-gray-100:hover {
background-color: rgb(75, 85, 99) !important;
}
/* Element Plus 组件样式覆盖 */
:deep(.el-input__wrapper) {
background-color: rgb(31, 41, 55) !important;
border-color: rgb(75, 85, 99) !important;
box-shadow: none !important;
}
:deep(.el-input__inner) {
color: rgb(209, 213, 219) !important;
background-color: transparent !important;
}
:deep(.el-input__inner::placeholder) {
color: rgb(156, 163, 175) !important;
}
:deep(.el-textarea__inner) {
color: rgb(209, 213, 219) !important;
background-color: transparent !important;
}
:deep(.el-textarea__inner::placeholder) {
color: rgb(156, 163, 175) !important;
}
:deep(.el-switch__core) {
background-color: rgb(75, 85, 99) !important;
border-color: rgb(75, 85, 99) !important;
}
:deep(.el-switch.is-checked .el-switch__core) {
background-color: rgb(139, 92, 246) !important;
border-color: rgb(139, 92, 246) !important;
}
:deep(.el-slider__runway) {
background-color: rgb(75, 85, 99) !important;
}
:deep(.el-slider__bar) {
background-color: rgb(139, 92, 246) !important;
}
:deep(.el-slider__button) {
border-color: rgb(139, 92, 246) !important;
}
:deep(.el-tooltip__trigger) {
color: rgb(156, 163, 175) !important;
}
}
/* 提交按钮 */
.bg-gradient-to-r {
background: linear-gradient(88deg, #af61f0 1.44%, #5b62ce) !important;
&:hover {
background: linear-gradient(88deg, #9f51e0 1.44%, #4b52be) !important;
}
&:disabled {
background: linear-gradient(88deg, #6b7280 1.44%, #4b5563) !important;
}
}
/* 作品列表 */
.jimeng-create__works {
&-title {
color: rgb(255, 255, 255) !important;
}
&-item {
background-color: rgb(55, 65, 81) !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
&-content {
.jimeng-create__works-item-info {
&-title {
color: rgb(255, 255, 255) !important;
}
&-prompt {
color: rgb(209, 213, 219) !important;
}
&-tags {
&-item {
background-color: rgb(75, 85, 99) !important;
color: rgb(209, 213, 219) !important;
&--warning {
background-color: rgb(239, 68, 68) !important;
color: rgb(255, 255, 255) !important;
}
&--primary {
background-color: rgb(59, 130, 246) !important;
color: rgb(255, 255, 255) !important;
}
&--power {
background-color: rgb(139, 92, 246) !important;
color: rgb(255, 255, 255) !important;
}
}
}
}
}
&-quick-actions {
button {
color: rgb(156, 163, 175) !important;
&:hover {
color: rgb(209, 213, 219) !important;
}
}
}
&-error {
&-content {
background-color: rgb(31, 41, 55) !important;
border-color: rgb(239, 68, 68) !important;
.jimeng-create__works-item-error-text {
color: rgb(239, 68, 68) !important;
}
.jimeng-create__works-item-error-copy-btn {
color: rgb(156, 163, 175) !important;
&:hover {
color: rgb(209, 213, 219) !important;
}
}
}
}
}
&-loading {
color: rgb(156, 163, 175) !important;
}
&-finished {
color: rgb(156, 163, 175) !important;
}
}
/* 媒体预览弹窗 */
.jimeng-create__media-dialog {
background-color: rgba(0, 0, 0, 0.8) !important;
&-content {
background-color: rgb(55, 65, 81) !important;
box-shadow: 0 0 15px rgba(107, 80, 225, 0.8) !important;
}
&-header {
background-color: rgb(31, 41, 55) !important;
border-bottom-color: rgb(75, 85, 99) !important;
h3 {
color: rgb(255, 255, 255) !important;
}
button {
color: rgb(156, 163, 175) !important;
&:hover {
color: rgb(209, 213, 219) !important;
}
}
}
}
/* 图片上传组件 */
:deep(.image-upload) {
.upload-area {
background-color: rgb(31, 41, 55) !important;
border-color: rgb(75, 85, 99) !important;
&:hover {
border-color: rgb(139, 92, 246) !important;
background-color: rgb(55, 65, 81) !important;
}
}
.upload-text {
color: rgb(209, 213, 219) !important;
}
.upload-icon {
color: rgb(139, 92, 246) !important;
}
}
/* 自定义选择组件 */
:deep(.custom-select) {
.select-trigger {
background-color: rgb(31, 41, 55) !important;
border-color: rgb(75, 85, 99) !important;
color: rgb(209, 213, 219) !important;
}
.select-dropdown {
background-color: rgb(55, 65, 81) !important;
border-color: rgb(75, 85, 99) !important;
box-shadow: 0 0 15px rgba(107, 80, 225, 0.8) !important;
}
.select-option {
color: rgb(209, 213, 219) !important;
&:hover {
background-color: rgb(75, 85, 99) !important;
}
&.selected {
background-color: rgb(139, 92, 246) !important;
color: rgb(255, 255, 255) !important;
}
}
}
/* 空状态组件 */
:deep(.van-empty) {
.van-empty__description {
color: rgb(156, 163, 175) !important;
}
}
}
</style>