From 809d8d71bd965d5ba921bc3939d0e590f4ec4654 Mon Sep 17 00:00:00 2001 From: GeekMaster Date: Tue, 16 Sep 2025 19:13:41 +0800 Subject: [PATCH] Jimeng AI 4.0 for mobile is ready --- CHANGELOG.md | 2 +- web/src/assets/css/mobile/jimeng.scss | 281 ++++++++ web/src/store/jimeng.js | 2 + web/src/store/mobile/jimeng.js | 525 -------------- web/src/views/mobile/JimengCreate.vue | 947 ++++++-------------------- 5 files changed, 506 insertions(+), 1251 deletions(-) delete mode 100644 web/src/store/mobile/jimeng.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 18322bb6..f2ee79f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ - Bug 修复:修复超级管理员无法修改密码的 Bug - Bug 修复:微信登录配置更新后,没有同步更新到系统配置 - 功能优化: 给 AI 对话 API 加上线程锁,确保同一个用户同时只有一个对话请求 -- 功能新增:支持即梦 AI 4.0 图片编辑,即梦 AI 数字人,动作迁移功能 +- 功能新增:支持即梦 AI 4.0 图片编辑,即梦 AI 数字人,动作迁移功能。🔥🔥🔥 ## v4.2.6 diff --git a/web/src/assets/css/mobile/jimeng.scss b/web/src/assets/css/mobile/jimeng.scss index 382cdff4..a2fcf33a 100644 --- a/web/src/assets/css/mobile/jimeng.scss +++ b/web/src/assets/css/mobile/jimeng.scss @@ -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; + } + } +} diff --git a/web/src/store/jimeng.js b/web/src/store/jimeng.js index cf37412d..311170e0 100644 --- a/web/src/store/jimeng.js +++ b/web/src/store/jimeng.js @@ -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, diff --git a/web/src/store/mobile/jimeng.js b/web/src/store/mobile/jimeng.js deleted file mode 100644 index 300ce5cf..00000000 --- a/web/src/store/mobile/jimeng.js +++ /dev/null @@ -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, - } -}) diff --git a/web/src/views/mobile/JimengCreate.vue b/web/src/views/mobile/JimengCreate.vue index 2d25b2cb..61764c62 100644 --- a/web/src/views/mobile/JimengCreate.vue +++ b/web/src/views/mobile/JimengCreate.vue @@ -14,252 +14,45 @@ - +
- - - + +
+ + + + + +
- -
- -
-
- -
- -
- - -
-
- 图生图人像写真 - -
-
- - -
- -
- -
- - -
- -
- - 创意度: - - - - - -
- -
-
- -
-
- - -
-
-
-
- - - - -
-
-
- -
- -
- -
- -
- -
-
- -
- -
-
-
- - - - -
-
- -
- -
-
- -
- - - -
- -
-
- -
- -
-
-
- - - - -
-
-
- -
- -
- -
-
- - -
-
- -
- -
- -
-
- -
- -
-
-
-
+ +
+ +
-
+
@@ -267,215 +60,209 @@

我的作品

-
-
-
-
-
- - - -
-