mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-12 14:14:25 +08:00
优化移动端即梦页面
This commit is contained in:
787
web/src/assets/css/mobile/jimeng.scss
Normal file
787
web/src/assets/css/mobile/jimeng.scss
Normal file
@@ -0,0 +1,787 @@
|
||||
/* JimengCreate Mobile Styles */
|
||||
|
||||
/* 自定义动画 */
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-fade-out {
|
||||
animation: fade-out 0.3s ease-out;
|
||||
}
|
||||
|
||||
.animate-scale-up {
|
||||
animation: scale-up 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 文本截断 */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 深色模式适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.bg-gray-50 {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.text-gray-900 {
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.text-gray-700 {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
border-color: #4b5563;
|
||||
}
|
||||
|
||||
.bg-gray-100:hover {
|
||||
background-color: #4b5563;
|
||||
}
|
||||
}
|
||||
|
||||
/* 即梦创作页面专用样式 */
|
||||
.jimeng-create {
|
||||
min-height: 100vh;
|
||||
background-color: #f9fafb;
|
||||
|
||||
/* 页面头部样式 */
|
||||
&__header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
background-color: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
height: 3.5rem;
|
||||
}
|
||||
|
||||
&-back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
&-spacer {
|
||||
width: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
&__content {
|
||||
padding: 1rem;
|
||||
|
||||
.space-y-6 > * + * {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 功能分类选择 */
|
||||
&__category-section {
|
||||
background-color: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
&-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 2px solid;
|
||||
transition: all 0.2s;
|
||||
|
||||
&--active {
|
||||
border-color: #3b82f6;
|
||||
background-color: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
&--inactive {
|
||||
border-color: #e5e7eb;
|
||||
background-color: #f9fafb;
|
||||
color: #6b7280;
|
||||
|
||||
&:hover {
|
||||
border-color: #d1d5db;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 生成模式切换 */
|
||||
&__mode-section {
|
||||
background-color: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: #111827;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-description {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表单组件样式 */
|
||||
&__form-section {
|
||||
background-color: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&-label {
|
||||
display: block;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
&-textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.5rem;
|
||||
resize: none;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
ring: 2px solid #3b82f6;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&-counter {
|
||||
text-align: right;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
/* 图片上传样式 */
|
||||
&__upload {
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #60a5fa;
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
color: #3b82f6;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&-text {
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&-preview {
|
||||
position: relative;
|
||||
|
||||
.el-image {
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-remove-btn {
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
right: -0.5rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
background-color: #ef4444;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
/* 多图片上传样式 */
|
||||
&-multiple {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
|
||||
&-item {
|
||||
position: relative;
|
||||
|
||||
.el-image {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-add {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
border: 2px dashed #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #60a5fa;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
color: #9ca3af;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 滑块样式 */
|
||||
&__slider-section {
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-tooltip {
|
||||
.iconfont {
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 开关样式 */
|
||||
&__switch-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
span {
|
||||
color: #111827;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
/* 生成按钮 */
|
||||
&__submit-btn {
|
||||
position: sticky;
|
||||
bottom: 1rem;
|
||||
background-color: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(to right, #3b82f6, #8b5cf6);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border-radius: 0.75rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: linear-gradient(to right, #2563eb, #7c3aed);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: linear-gradient(to right, #9ca3af, #9ca3af);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
&.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 作品列表样式 */
|
||||
&__works {
|
||||
padding: 1rem;
|
||||
|
||||
&-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&-list {
|
||||
.space-y-4 > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
background-color: white;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
&-thumb {
|
||||
flex-shrink: 0;
|
||||
|
||||
&-container {
|
||||
position: relative;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f3f4f6;
|
||||
|
||||
.iconfont {
|
||||
color: #9ca3af;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
color: white;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&-status {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&--loading {
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
|
||||
.iconfont {
|
||||
color: #3b82f6;
|
||||
font-size: 1.25rem;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&--failed {
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
|
||||
.iconfont {
|
||||
color: #ef4444;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-title {
|
||||
color: #111827;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-prompt {
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
&-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&--failed {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&--processing {
|
||||
color: #2563eb;
|
||||
|
||||
.loading-spinner {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border: 1px solid #2563eb;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
&-item {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 9999px;
|
||||
|
||||
&--primary {
|
||||
background-color: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background-color: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
&--power {
|
||||
background-color: #fed7aa;
|
||||
color: #ea580c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 1rem;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
.iconfont {
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background-color: #2563eb;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #1d4ed8;
|
||||
}
|
||||
}
|
||||
|
||||
&--success {
|
||||
background-color: #16a34a;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #15803d;
|
||||
}
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background-color: #ea580c;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background-color: #fef2f2;
|
||||
color: #dc2626;
|
||||
|
||||
&:hover {
|
||||
background-color: #fecaca;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
|
||||
.iconfont {
|
||||
color: #3b82f6;
|
||||
font-size: 1.25rem;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&-finished {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
/* 媒体预览弹窗 */
|
||||
&__media-dialog {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
&-content {
|
||||
background-color: white;
|
||||
border-radius: 1rem;
|
||||
width: 100%;
|
||||
max-width: 56rem;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
|
||||
h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem;
|
||||
border: none;
|
||||
background: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-body {
|
||||
padding: 1.5rem;
|
||||
|
||||
img, video {
|
||||
width: 100%;
|
||||
max-height: 60vh;
|
||||
object-fit: contain;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div class="black-dialog">
|
||||
<el-dialog v-model="showDialog" :title="title" :width="width" :before-close="cancel">
|
||||
<div class="dialog-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<template #footer v-if="!hideFooter">
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel" style="--el-border-radius-base: 8px">{{
|
||||
cancelText
|
||||
}}</el-button>
|
||||
<el-button type="primary" @click="$emit('confirm')" v-if="!hideConfirm">{{
|
||||
confirmText
|
||||
}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Tips',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: 'auto',
|
||||
},
|
||||
hideFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideConfirm: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确定',
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
})
|
||||
const emits = defineEmits(['confirm', 'cancal'])
|
||||
const showDialog = ref(props.show)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(newValue) => {
|
||||
showDialog.value = newValue
|
||||
}
|
||||
)
|
||||
const cancel = () => {
|
||||
showDialog.value = false
|
||||
emits('cancal')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.black-dialog {
|
||||
.dialog-body {
|
||||
.form {
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
font-family: 'Neue Montreal';
|
||||
padding: 10px 0;
|
||||
|
||||
.label {
|
||||
margin-bottom: 0.6rem;
|
||||
margin-inline-end: 0.75rem;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
background: none;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #8f8f8f;
|
||||
outline: none;
|
||||
transition: border-color 0.5s ease, box-shadow 0.5s ease;
|
||||
|
||||
&:focus {
|
||||
border-color: #0f7a71;
|
||||
box-shadow: 0 0 5px #0f7a71;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<el-select
|
||||
v-model="model"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
@change="$emit('update:value', $event)"
|
||||
style="--el-border-radius-base: 20px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "BlackSelect",
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请选择"
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
model: this.value
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<el-switch
|
||||
v-model="model"
|
||||
:size="size"
|
||||
@change="$emit('update:value', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
const props = defineProps({
|
||||
value: Boolean,
|
||||
size: {
|
||||
type: String,
|
||||
default: "default"
|
||||
}
|
||||
});
|
||||
const model = ref(props.value);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
model.value = newValue;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -361,19 +361,19 @@ const routes = [
|
||||
meta: { title: 'Suno音乐创作' },
|
||||
path: '/mobile/suno',
|
||||
name: 'mobile-suno',
|
||||
component: () => import('@/views/mobile/pages/SunoCreate.vue'),
|
||||
component: () => import('@/views/mobile/SunoCreate.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '视频生成' },
|
||||
path: '/mobile/video',
|
||||
name: 'mobile-video',
|
||||
component: () => import('@/views/mobile/pages/VideoCreate.vue'),
|
||||
component: () => import('@/views/mobile/VideoCreate.vue'),
|
||||
},
|
||||
{
|
||||
meta: { title: '即梦AI' },
|
||||
path: '/mobile/jimeng',
|
||||
name: 'mobile-jimeng',
|
||||
component: () => import('@/views/mobile/pages/JimengCreate.vue'),
|
||||
component: () => import('@/views/mobile/JimengCreate.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
410
web/src/store/mobile/jimeng.js
Normal file
410
web/src/store/mobile/jimeng.js
Normal file
@@ -0,0 +1,410 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { httpGet, httpPost } from '@/utils/http'
|
||||
import { showMessageError, showMessageOK, showLoading, closeLoading } from '@/utils/dialog'
|
||||
import { showConfirmDialog } from 'vant'
|
||||
|
||||
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 categories = ref([
|
||||
{ key: 'image_generation', name: '图像生成' },
|
||||
{ key: 'image_editing', name: '图像编辑' },
|
||||
{ key: 'image_effects', name: '图像特效' },
|
||||
{ key: 'video_generation', name: '视频生成' },
|
||||
])
|
||||
|
||||
// 选项数据
|
||||
const imageSizeOptions = [
|
||||
{ label: '512x512', value: '512x512' },
|
||||
{ label: '768x768', value: '768x768' },
|
||||
{ label: '1024x1024', value: '1024x1024' },
|
||||
{ label: '1024x1536', value: '1024x1536' },
|
||||
{ label: '1536x1024', value: '1536x1024' },
|
||||
]
|
||||
|
||||
const videoAspectRatioOptions = [
|
||||
{ label: '16:9', value: '16:9' },
|
||||
{ label: '9:16', value: '9:16' },
|
||||
{ label: '1:1', value: '1:1' },
|
||||
{ label: '4:3', value: '4:3' },
|
||||
]
|
||||
|
||||
const imageEffectsTemplateOptions = [
|
||||
{ label: '亚克力装饰', value: 'acrylic_ornaments' },
|
||||
{ label: '天使小雕像', value: 'angel_figurine' },
|
||||
{ label: '毛毫3D拍立得', value: 'felt_3d_polaroid' },
|
||||
{ label: '水彩插图', value: 'watercolor_illustration' },
|
||||
]
|
||||
|
||||
// 功能参数
|
||||
const textToImageParams = ref({
|
||||
size: '1024x1024',
|
||||
scale: 7.5,
|
||||
use_pre_llm: false,
|
||||
})
|
||||
|
||||
const imageToImageParams = ref({
|
||||
image_input: [],
|
||||
size: '1024x1024',
|
||||
})
|
||||
|
||||
const imageEditParams = ref({
|
||||
image_urls: [],
|
||||
scale: 0.5,
|
||||
})
|
||||
|
||||
const imageEffectsParams = ref({
|
||||
image_input1: [],
|
||||
template_id: '',
|
||||
size: '1024x1024',
|
||||
})
|
||||
|
||||
const textToVideoParams = ref({
|
||||
aspect_ratio: '16:9',
|
||||
})
|
||||
|
||||
const imageToVideoParams = ref({
|
||||
image_urls: [],
|
||||
aspect_ratio: '16:9',
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
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 === 'video_generation') {
|
||||
return useImageInput.value ? 'image_to_video' : 'text_to_video'
|
||||
}
|
||||
return 'text_to_image'
|
||||
})
|
||||
|
||||
// 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 switchInputMode = () => {
|
||||
currentPrompt.value = ''
|
||||
}
|
||||
|
||||
const handleMultipleImageUpload = (event) => {
|
||||
const files = Array.from(event.target.files)
|
||||
files.forEach((file) => {
|
||||
if (imageToVideoParams.value.image_urls.length < 2) {
|
||||
onImageUpload({ file, name: file.name })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const removeImage = (index) => {
|
||||
imageToVideoParams.value.image_urls.splice(index, 1)
|
||||
}
|
||||
|
||||
const onImageUpload = (file) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.file, file.name)
|
||||
showLoading('正在上传图片...')
|
||||
|
||||
return httpPost('/api/upload', formData)
|
||||
.then((res) => {
|
||||
showMessageOK('图片上传成功')
|
||||
const imageData = { url: res.data.url, content: res.data.url }
|
||||
|
||||
// 根据当前活动功能添加到相应的参数中
|
||||
if (activeFunction.value === 'image_to_image') {
|
||||
imageToImageParams.value.image_input = [imageData]
|
||||
} else if (activeFunction.value === 'image_edit') {
|
||||
imageEditParams.value.image_urls = [imageData]
|
||||
} else if (activeFunction.value === 'image_effects') {
|
||||
imageEffectsParams.value.image_input1 = [imageData]
|
||||
} else if (activeFunction.value === 'image_to_video') {
|
||||
imageToVideoParams.value.image_urls.push(imageData)
|
||||
}
|
||||
|
||||
return res.data.url
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError('图片上传失败:' + e.message)
|
||||
})
|
||||
.finally(() => {
|
||||
closeLoading()
|
||||
})
|
||||
}
|
||||
|
||||
const submitTask = () => {
|
||||
if (!currentPrompt.value.trim()) {
|
||||
showMessageError('请输入提示词')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
const params = {
|
||||
type: activeFunction.value,
|
||||
prompt: currentPrompt.value,
|
||||
}
|
||||
|
||||
// 根据功能类型添加相应参数
|
||||
if (activeFunction.value === 'text_to_image') {
|
||||
Object.assign(params, textToImageParams.value)
|
||||
} else if (activeFunction.value === 'image_to_image') {
|
||||
Object.assign(params, imageToImageParams.value)
|
||||
} else if (activeFunction.value === 'image_edit') {
|
||||
Object.assign(params, imageEditParams.value)
|
||||
} else if (activeFunction.value === 'image_effects') {
|
||||
Object.assign(params, imageEffectsParams.value)
|
||||
} else if (activeFunction.value === 'text_to_video') {
|
||||
Object.assign(params, textToVideoParams.value)
|
||||
} else if (activeFunction.value === 'image_to_video') {
|
||||
Object.assign(params, imageToVideoParams.value)
|
||||
}
|
||||
|
||||
return httpPost('/api/jimeng/create', params)
|
||||
.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 httpGet('/api/jimeng/list', { page: page.value, page_size: pageSize.value })
|
||||
.then((res) => {
|
||||
total.value = res.data.total
|
||||
let needPull = false
|
||||
const 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 = (item) => {
|
||||
item.downloading = true
|
||||
const link = document.createElement('a')
|
||||
link.href = item.img_url || item.video_url
|
||||
link.download = item.title || 'file'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
item.downloading = false
|
||||
showMessageSuccess('开始下载')
|
||||
}
|
||||
|
||||
const retryTask = (id) => {
|
||||
return httpPost('/api/jimeng/retry', { id })
|
||||
.then(() => {
|
||||
showMessageOK('重试任务成功')
|
||||
fetchData(1)
|
||||
})
|
||||
.catch((e) => {
|
||||
showMessageError('重试任务失败:' + e.message)
|
||||
})
|
||||
}
|
||||
|
||||
const removeJob = (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 resetParams = () => {
|
||||
textToImageParams.value = {
|
||||
size: '1024x1024',
|
||||
scale: 7.5,
|
||||
use_pre_llm: false,
|
||||
}
|
||||
imageToImageParams.value = {
|
||||
image_input: [],
|
||||
size: '1024x1024',
|
||||
}
|
||||
imageEditParams.value = {
|
||||
image_urls: [],
|
||||
scale: 0.5,
|
||||
}
|
||||
imageEffectsParams.value = {
|
||||
image_input1: [],
|
||||
template_id: '',
|
||||
size: '1024x1024',
|
||||
}
|
||||
textToVideoParams.value = {
|
||||
aspect_ratio: '16:9',
|
||||
}
|
||||
imageToVideoParams.value = {
|
||||
image_urls: [],
|
||||
aspect_ratio: '16:9',
|
||||
}
|
||||
}
|
||||
|
||||
const closeMediaDialog = () => {
|
||||
showMediaDialog.value = false
|
||||
currentMediaUrl.value = ''
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
// Computed
|
||||
activeFunction,
|
||||
|
||||
// Actions
|
||||
getCategoryIcon,
|
||||
switchCategory,
|
||||
switchInputMode,
|
||||
handleMultipleImageUpload,
|
||||
removeImage,
|
||||
onImageUpload,
|
||||
submitTask,
|
||||
fetchData,
|
||||
loadMore,
|
||||
playMedia,
|
||||
downloadFile,
|
||||
retryTask,
|
||||
removeJob,
|
||||
getFunctionName,
|
||||
getTaskType,
|
||||
startTaskPolling,
|
||||
stopTaskPolling,
|
||||
resetParams,
|
||||
closeMediaDialog,
|
||||
}
|
||||
})
|
||||
706
web/src/views/mobile/JimengCreate.vue
Normal file
706
web/src/views/mobile/JimengCreate.vue
Normal file
@@ -0,0 +1,706 @@
|
||||
<template>
|
||||
<div class="jimeng-create">
|
||||
<!-- 页面头部 -->
|
||||
<div class="jimeng-create__header">
|
||||
<div class="jimeng-create__header-content">
|
||||
<button @click="goBack" class="jimeng-create__header-back-btn">
|
||||
<i class="iconfont icon-back"></i>
|
||||
</button>
|
||||
<h1 class="jimeng-create__header-title">即梦AI</h1>
|
||||
<div class="jimeng-create__header-spacer"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 功能分类选择 -->
|
||||
<div class="jimeng-create__content">
|
||||
<div class="jimeng-create__category-section">
|
||||
<CustomTabs
|
||||
v-model="jimengStore.activeCategory"
|
||||
@update:modelValue="jimengStore.switchCategory"
|
||||
>
|
||||
<CustomTabPane
|
||||
v-for="category in jimengStore.categories"
|
||||
:key="category.key"
|
||||
:label="category.name"
|
||||
:name="category.key"
|
||||
>
|
||||
<template #label>
|
||||
<span>{{ category.name }}</span>
|
||||
</template>
|
||||
</CustomTabPane>
|
||||
</CustomTabs>
|
||||
</div>
|
||||
<!-- 生成模式切换 -->
|
||||
<div
|
||||
v-if="
|
||||
jimengStore.activeCategory === 'image_generation' ||
|
||||
jimengStore.activeCategory === 'video_generation'
|
||||
"
|
||||
class="jimeng-create__mode-section"
|
||||
>
|
||||
<div class="jimeng-create__mode-section-content">
|
||||
<div>
|
||||
<span class="jimeng-create__mode-section-title">生成模式</span>
|
||||
<p class="jimeng-create__mode-section-description">
|
||||
{{
|
||||
jimengStore.activeCategory === 'image_generation' ? '图生图人像写真' : '图生视频'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="jimengStore.useImageInput"
|
||||
@change="jimengStore.switchInputMode"
|
||||
size="default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文生图 -->
|
||||
<div v-if="jimengStore.activeFunction === 'text_to_image'" class="space-y-6">
|
||||
<!-- 提示词输入 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">提示词</label>
|
||||
<textarea
|
||||
v-model="jimengStore.currentPrompt"
|
||||
placeholder="请输入图片描述,越详细越好"
|
||||
class="jimeng-create__form-section-textarea"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
/>
|
||||
<div class="jimeng-create__form-section-counter">
|
||||
<span>{{ jimengStore.currentPrompt.length }}/2000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片尺寸 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.textToImageParams.size"
|
||||
:options="
|
||||
jimengStore.imageSizeOptions.map((opt) => ({ label: opt.label, value: opt.value }))
|
||||
"
|
||||
label="图片尺寸"
|
||||
title="选择尺寸"
|
||||
/>
|
||||
|
||||
<!-- 创意度 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<div class="jimeng-create__slider-section">
|
||||
<div class="jimeng-create__slider-section-header">
|
||||
<label>创意度</label>
|
||||
<el-tooltip content="创意度越高,影响文本描述的程度越高" placement="top">
|
||||
<i class="iconfont icon-info"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-slider
|
||||
v-model="jimengStore.textToImageParams.scale"
|
||||
:min="1"
|
||||
:max="10"
|
||||
:step="0.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 智能优化提示词 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<div class="jimeng-create__switch-section">
|
||||
<span>智能优化提示词</span>
|
||||
<el-switch v-model="jimengStore.textToImageParams.use_pre_llm" size="default" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图生图 -->
|
||||
<div v-if="jimengStore.activeFunction === 'image_to_image'" class="space-y-6">
|
||||
<!-- 上传图片 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">上传图片</label>
|
||||
<div class="jimeng-create__upload">
|
||||
<input
|
||||
ref="imageToImageInput"
|
||||
type="file"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
@change="
|
||||
(e) =>
|
||||
jimengStore.onImageUpload({
|
||||
file: e.target.files[0],
|
||||
name: e.target.files[0]?.name,
|
||||
})
|
||||
"
|
||||
class="hidden"
|
||||
/>
|
||||
<div @click="$refs.imageToImageInput?.click()" class="jimeng-create__upload-content">
|
||||
<i
|
||||
v-if="!jimengStore.imageToImageParams.image_input.length"
|
||||
class="jimeng-create__upload-icon iconfont icon-upload"
|
||||
></i>
|
||||
<span
|
||||
v-if="!jimengStore.imageToImageParams.image_input.length"
|
||||
class="jimeng-create__upload-text"
|
||||
>上传图片</span
|
||||
>
|
||||
<div v-else class="jimeng-create__upload-preview">
|
||||
<el-image
|
||||
:src="
|
||||
jimengStore.imageToImageParams.image_input[0]?.url ||
|
||||
jimengStore.imageToImageParams.image_input[0]?.content
|
||||
"
|
||||
fit="cover"
|
||||
class="w-32 h-32 rounded"
|
||||
/>
|
||||
<button
|
||||
@click.stop="jimengStore.imageToImageParams.image_input = []"
|
||||
class="jimeng-create__upload-remove-btn"
|
||||
>
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示词输入 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">提示词</label>
|
||||
<textarea
|
||||
v-model="jimengStore.currentPrompt"
|
||||
placeholder="描述你想要的图片效果"
|
||||
class="jimeng-create__form-section-textarea"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
/>
|
||||
<div class="jimeng-create__form-section-counter">
|
||||
<span>{{ jimengStore.currentPrompt.length }}/2000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片尺寸 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.imageToImageParams.size"
|
||||
:options="
|
||||
jimengStore.imageSizeOptions.map((opt) => ({ label: opt.label, value: opt.value }))
|
||||
"
|
||||
label="图片尺寸"
|
||||
title="选择尺寸"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 图像编辑 -->
|
||||
<div v-if="jimengStore.activeFunction === 'image_edit'" class="space-y-6">
|
||||
<!-- 上传图片 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">上传图片</label>
|
||||
<div class="jimeng-create__upload">
|
||||
<input
|
||||
ref="imageEditInput"
|
||||
type="file"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
@change="
|
||||
(e) =>
|
||||
jimengStore.onImageUpload({
|
||||
file: e.target.files[0],
|
||||
name: e.target.files[0]?.name,
|
||||
})
|
||||
"
|
||||
class="hidden"
|
||||
/>
|
||||
<div @click="$refs.imageEditInput?.click()" class="jimeng-create__upload-content">
|
||||
<i
|
||||
v-if="!jimengStore.imageEditParams.image_urls.length"
|
||||
class="jimeng-create__upload-icon iconfont icon-upload"
|
||||
></i>
|
||||
<span
|
||||
v-if="!jimengStore.imageEditParams.image_urls.length"
|
||||
class="jimeng-create__upload-text"
|
||||
>上传图片</span
|
||||
>
|
||||
<div v-else class="jimeng-create__upload-preview">
|
||||
<el-image
|
||||
:src="
|
||||
jimengStore.imageEditParams.image_urls[0]?.url ||
|
||||
jimengStore.imageEditParams.image_urls[0]?.content
|
||||
"
|
||||
fit="cover"
|
||||
class="w-32 h-32 rounded"
|
||||
/>
|
||||
<button
|
||||
@click.stop="jimengStore.imageEditParams.image_urls = []"
|
||||
class="jimeng-create__upload-remove-btn"
|
||||
>
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑提示词 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">编辑提示词</label>
|
||||
<textarea
|
||||
v-model="jimengStore.currentPrompt"
|
||||
placeholder="描述你想要的编辑效果"
|
||||
class="jimeng-create__form-section-textarea"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
/>
|
||||
<div class="jimeng-create__form-section-counter">
|
||||
<span>{{ jimengStore.currentPrompt.length }}/2000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑强度 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<div class="jimeng-create__slider-section">
|
||||
<label>编辑强度</label>
|
||||
<el-slider v-model="jimengStore.imageEditParams.scale" :min="0" :max="1" :step="0.1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图像特效 -->
|
||||
<div v-if="jimengStore.activeFunction === 'image_effects'" class="space-y-6">
|
||||
<!-- 上传图片 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">上传图片</label>
|
||||
<div class="jimeng-create__upload">
|
||||
<input
|
||||
ref="imageEffectsInput"
|
||||
type="file"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
@change="
|
||||
(e) =>
|
||||
jimengStore.onImageUpload({
|
||||
file: e.target.files[0],
|
||||
name: e.target.files[0]?.name,
|
||||
})
|
||||
"
|
||||
class="hidden"
|
||||
/>
|
||||
<div @click="$refs.imageEffectsInput?.click()" class="jimeng-create__upload-content">
|
||||
<i
|
||||
v-if="!jimengStore.imageEffectsParams.image_input1.length"
|
||||
class="jimeng-create__upload-icon iconfont icon-upload"
|
||||
></i>
|
||||
<span
|
||||
v-if="!jimengStore.imageEffectsParams.image_input1.length"
|
||||
class="jimeng-create__upload-text"
|
||||
>上传图片</span
|
||||
>
|
||||
<div v-else class="jimeng-create__upload-preview">
|
||||
<el-image
|
||||
:src="
|
||||
jimengStore.imageEffectsParams.image_input1[0]?.url ||
|
||||
jimengStore.imageEffectsParams.image_input1[0]?.content
|
||||
"
|
||||
fit="cover"
|
||||
class="w-32 h-32 rounded"
|
||||
/>
|
||||
<button
|
||||
@click.stop="jimengStore.imageEffectsParams.image_input1 = []"
|
||||
class="jimeng-create__upload-remove-btn"
|
||||
>
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 特效模板 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.imageEffectsParams.template_id"
|
||||
:options="
|
||||
jimengStore.imageEffectsTemplateOptions.map((opt) => ({
|
||||
label: opt.label,
|
||||
value: opt.value,
|
||||
}))
|
||||
"
|
||||
label="特效模板"
|
||||
title="选择特效模板"
|
||||
/>
|
||||
|
||||
<!-- 输出尺寸 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.imageEffectsParams.size"
|
||||
:options="
|
||||
jimengStore.imageSizeOptions.map((opt) => ({ label: opt.label, value: opt.value }))
|
||||
"
|
||||
label="输出尺寸"
|
||||
title="选择尺寸"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 文生视频 -->
|
||||
<div v-if="jimengStore.activeFunction === 'text_to_video'" class="space-y-6">
|
||||
<!-- 提示词输入 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">提示词</label>
|
||||
<textarea
|
||||
v-model="jimengStore.currentPrompt"
|
||||
placeholder="描述你想要的视频内容"
|
||||
class="jimeng-create__form-section-textarea"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
/>
|
||||
<div class="jimeng-create__form-section-counter">
|
||||
<span>{{ jimengStore.currentPrompt.length }}/2000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频比例 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.textToVideoParams.aspect_ratio"
|
||||
:options="
|
||||
jimengStore.videoAspectRatioOptions.map((opt) => ({
|
||||
label: opt.label,
|
||||
value: opt.value,
|
||||
}))
|
||||
"
|
||||
label="视频比例"
|
||||
title="选择比例"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 图生视频 -->
|
||||
<div v-if="jimengStore.activeFunction === 'image_to_video'" class="space-y-6">
|
||||
<!-- 上传图片 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">上传图片(最多2张)</label>
|
||||
<div class="jimeng-create__upload">
|
||||
<input
|
||||
ref="imageToVideoInput"
|
||||
type="file"
|
||||
accept=".jpg,.png,.jpeg"
|
||||
multiple
|
||||
@change="(e) => jimengStore.handleMultipleImageUpload(e)"
|
||||
class="hidden"
|
||||
/>
|
||||
<div @click="$refs.imageToVideoInput?.click()" class="jimeng-create__upload-content">
|
||||
<i
|
||||
v-if="!jimengStore.imageToVideoParams.image_urls.length"
|
||||
class="jimeng-create__upload-icon iconfont icon-upload"
|
||||
></i>
|
||||
<span
|
||||
v-if="!jimengStore.imageToVideoParams.image_urls.length"
|
||||
class="jimeng-create__upload-text"
|
||||
>上传图片</span
|
||||
>
|
||||
<div v-else class="jimeng-create__upload-multiple">
|
||||
<div
|
||||
v-for="(image, index) in jimengStore.imageToVideoParams.image_urls"
|
||||
:key="index"
|
||||
class="jimeng-create__upload-multiple-item"
|
||||
>
|
||||
<el-image
|
||||
:src="image?.url || image?.content"
|
||||
fit="cover"
|
||||
class="w-24 h-24 rounded"
|
||||
/>
|
||||
<button
|
||||
@click.stop="jimengStore.removeImage(index)"
|
||||
class="jimeng-create__upload-remove-btn"
|
||||
>
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="jimengStore.imageToVideoParams.image_urls.length < 2"
|
||||
@click.stop="$refs.imageToVideoInput?.click()"
|
||||
class="jimeng-create__upload-multiple-add"
|
||||
>
|
||||
<i class="iconfont icon-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示词输入 -->
|
||||
<div class="jimeng-create__form-section">
|
||||
<label class="jimeng-create__form-section-label">提示词</label>
|
||||
<textarea
|
||||
v-model="jimengStore.currentPrompt"
|
||||
placeholder="描述你想要的视频效果"
|
||||
class="jimeng-create__form-section-textarea"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
/>
|
||||
<div class="jimeng-create__form-section-counter">
|
||||
<span>{{ jimengStore.currentPrompt.length }}/2000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频比例 -->
|
||||
<CustomSelect
|
||||
v-model="jimengStore.imageToVideoParams.aspect_ratio"
|
||||
:options="
|
||||
jimengStore.videoAspectRatioOptions.map((opt) => ({
|
||||
label: opt.label,
|
||||
value: opt.value,
|
||||
}))
|
||||
"
|
||||
label="视频比例"
|
||||
title="选择比例"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 生成按钮 -->
|
||||
<div class="jimeng-create__submit-btn">
|
||||
<button @click="jimengStore.submitTask" :disabled="jimengStore.submitting">
|
||||
<i v-if="jimengStore.submitting" class="iconfont icon-loading animate-spin"></i>
|
||||
<span>{{
|
||||
jimengStore.submitting ? '创作中...' : `立即生成 (${jimengStore.currentPowerCost}算力)`
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作品列表 -->
|
||||
<div class="jimeng-create__works">
|
||||
<h2 class="jimeng-create__works-title">我的作品</h2>
|
||||
<div class="jimeng-create__works-list space-y-4">
|
||||
<div
|
||||
v-for="item in jimengStore.currentList"
|
||||
:key="item.id"
|
||||
class="jimeng-create__works-item"
|
||||
>
|
||||
<div class="jimeng-create__works-item-content">
|
||||
<div class="jimeng-create__works-item-thumb">
|
||||
<div class="jimeng-create__works-item-thumb-container">
|
||||
<el-image
|
||||
v-if="item.img_url"
|
||||
:src="item.img_url"
|
||||
fit="cover"
|
||||
class="w-full h-full"
|
||||
:preview-disabled="true"
|
||||
>
|
||||
<template #error>
|
||||
<div class="jimeng-create__works-item-thumb-placeholder">
|
||||
<i class="iconfont icon-image"></i>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-image
|
||||
v-else-if="item.video_url"
|
||||
:src="item.video_url"
|
||||
fit="cover"
|
||||
class="w-full h-full"
|
||||
:preview-disabled="true"
|
||||
>
|
||||
<template #error>
|
||||
<div class="jimeng-create__works-item-thumb-placeholder">
|
||||
<i class="iconfont icon-video"></i>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="jimeng-create__works-item-thumb-placeholder">
|
||||
<i
|
||||
:class="
|
||||
item.type.includes('video') ? 'iconfont icon-video' : 'iconfont icon-image'
|
||||
"
|
||||
></i>
|
||||
</div>
|
||||
<!-- 播放/查看按钮 -->
|
||||
<button
|
||||
v-if="item.status === 'completed'"
|
||||
@click="jimengStore.playMedia(item)"
|
||||
class="jimeng-create__works-item-thumb-overlay"
|
||||
>
|
||||
<i
|
||||
:class="
|
||||
item.type.includes('video') ? 'iconfont icon-play' : 'iconfont icon-eye'
|
||||
"
|
||||
></i>
|
||||
</button>
|
||||
<!-- 进度动画 -->
|
||||
<div
|
||||
v-if="item.status === 'in_queue' || item.status === 'generating'"
|
||||
class="jimeng-create__works-item-thumb-status jimeng-create__works-item-thumb-status--loading"
|
||||
>
|
||||
<i class="iconfont icon-loading animate-spin"></i>
|
||||
</div>
|
||||
<!-- 失败状态 -->
|
||||
<div
|
||||
v-if="item.status === 'failed'"
|
||||
class="jimeng-create__works-item-thumb-status jimeng-create__works-item-thumb-status--failed"
|
||||
>
|
||||
<i class="iconfont icon-warning"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jimeng-create__works-item-info">
|
||||
<div class="jimeng-create__works-item-info-header">
|
||||
<div class="flex-1">
|
||||
<h3 class="jimeng-create__works-item-info-title">
|
||||
{{ jimengStore.getFunctionName(item.type) }}
|
||||
</h3>
|
||||
<p class="jimeng-create__works-item-info-prompt line-clamp-2">
|
||||
{{ item.prompt }}
|
||||
</p>
|
||||
</div>
|
||||
<!-- 任务状态 -->
|
||||
<div
|
||||
v-if="item.status !== 'completed'"
|
||||
class="jimeng-create__works-item-info-status"
|
||||
>
|
||||
<div
|
||||
v-if="item.status === 'failed'"
|
||||
class="jimeng-create__works-item-info-status--failed"
|
||||
>
|
||||
<i class="iconfont icon-warning"></i>
|
||||
<span>失败</span>
|
||||
</div>
|
||||
<div v-else class="jimeng-create__works-item-info-status--processing">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>生成中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 标签 -->
|
||||
<div class="jimeng-create__works-item-info-tags">
|
||||
<span
|
||||
:class="[
|
||||
'jimeng-create__works-item-info-tags-item',
|
||||
jimengStore.getTaskType(item.type) === 'warning'
|
||||
? 'jimeng-create__works-item-info-tags-item--warning'
|
||||
: 'jimeng-create__works-item-info-tags-item--primary',
|
||||
]"
|
||||
>
|
||||
{{ jimengStore.getFunctionName(item.type) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="item.power"
|
||||
class="jimeng-create__works-item-info-tags-item jimeng-create__works-item-info-tags-item--power"
|
||||
>
|
||||
{{ item.power }}算力
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="jimeng-create__works-item-actions">
|
||||
<div class="jimeng-create__works-item-actions-left">
|
||||
<button
|
||||
v-if="item.status === 'completed'"
|
||||
@click="jimengStore.playMedia(item)"
|
||||
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--primary"
|
||||
>
|
||||
<i
|
||||
:class="item.type.includes('video') ? 'iconfont icon-play' : 'iconfont icon-eye'"
|
||||
></i>
|
||||
<span>{{ item.type.includes('video') ? '播放' : '查看' }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="item.status === 'completed'"
|
||||
@click="jimengStore.downloadFile(item)"
|
||||
:disabled="item.downloading"
|
||||
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--success"
|
||||
>
|
||||
<i v-if="item.downloading" class="iconfont icon-loading animate-spin"></i>
|
||||
<i v-else class="iconfont icon-download"></i>
|
||||
<span>{{ item.downloading ? '下载中...' : '下载' }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="item.status === 'failed'"
|
||||
@click="jimengStore.retryTask(item.id)"
|
||||
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--warning"
|
||||
>
|
||||
<i class="iconfont icon-refresh"></i>
|
||||
<span>重试</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="jimengStore.removeJob(item)"
|
||||
class="jimeng-create__works-item-actions-btn jimeng-create__works-item-actions-btn--danger"
|
||||
>
|
||||
<i class="iconfont icon-remove"></i>
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<div v-if="jimengStore.listLoading" class="jimeng-create__works-loading">
|
||||
<i class="iconfont icon-loading animate-spin"></i>
|
||||
</div>
|
||||
|
||||
<!-- 没有更多了 -->
|
||||
<div
|
||||
v-if="jimengStore.listFinished && !jimengStore.listLoading"
|
||||
class="jimeng-create__works-finished"
|
||||
>
|
||||
没有更多了
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 媒体预览弹窗 -->
|
||||
<div
|
||||
v-if="jimengStore.showMediaDialog"
|
||||
class="jimeng-create__media-dialog"
|
||||
@click="jimengStore.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">
|
||||
<i class="iconfont icon-close"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="jimeng-create__media-dialog-body">
|
||||
<img
|
||||
v-if="jimengStore.currentMediaUrl && !jimengStore.currentMediaUrl.includes('video')"
|
||||
:src="jimengStore.currentMediaUrl"
|
||||
class="w-full max-h-[60vh] object-contain rounded-lg"
|
||||
/>
|
||||
<video
|
||||
v-else-if="jimengStore.currentMediaUrl"
|
||||
:src="jimengStore.currentMediaUrl"
|
||||
controls
|
||||
autoplay
|
||||
class="w-full max-h-[60vh] rounded-lg"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useJimengStore } from '@/store/mobile/jimeng'
|
||||
import CustomSelect from '@/components/mobile/CustomSelect.vue'
|
||||
import CustomTabs from '@/components/ui/CustomTabs.vue'
|
||||
import CustomTabPane from '@/components/ui/CustomTabPane.vue'
|
||||
import { checkSession } from '@/store/cache'
|
||||
|
||||
const router = useRouter()
|
||||
const jimengStore = useJimengStore()
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
checkSession()
|
||||
.then(() => {
|
||||
jimengStore.fetchData(1)
|
||||
jimengStore.startTaskPolling()
|
||||
})
|
||||
.catch(() => {})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
jimengStore.stopTaskPolling()
|
||||
})
|
||||
|
||||
// 工具方法
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/css/mobile/jimeng.scss';
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user