This commit is contained in:
王佑琳 2026-03-09 01:43:28 +08:00
commit cc3eeb001f
64 changed files with 9227 additions and 0 deletions

23
.env.development Normal file
View File

@ -0,0 +1,23 @@
# 地址前缀
VITE_BASE = '/'
# 主服务
VITE_API_PREFIX = '/api'
VITE_API_BASE_URL = 'http://43.248.131.153:8001' # http://huanda.xueai.art http://106.54.11.219/api 43.248.131.153:8003
VITE_API_WS_URL = 'ws://huanda.xueai.art/api'
# 支付服务
VITE_API_PAY_PREFIX = '/pay'
VITE_API_PAY_TARGET = 'http://43.248.131.153:8091' # http://43.248.133.202 test.xueai.art
# AI Music后端服务
VITE_API_WORKFLOW_UPLOAD = 'http://43.248.131.153:8060/workflow/file/upload' # https://sxwz.xueai.art/workflow https://designtools.xueai.art/workflow
VITE_API_WORKFLOW_WS = 'ws://43.248.97.19:4000/huanda' # 43.248.131.153 43.248.131.153:8084
# 是否开启开发者工具
VITE_OPEN_DEVTOOLS = false
# 是否开启KKFileView
FILE_OPEN_PREVIEW = true
# KKFileView服务器地址
# FILE_VIEW_SERVER_URL = 'http://192.168.122.209:8012'

23
.env.production Normal file
View File

@ -0,0 +1,23 @@
# 地址前缀
VITE_BASE = '/'
# 是否在打包时启用 Mock
VITE_BUILD_MOCK = false
# 主服务
VITE_API_PREFIX = '/api'
VITE_API_BASE_URL = 'https://huanda.xueai.art/api'
VITE_API_WS_URL = 'wss://huanda.xueai.art/api'
# 支付服务
VITE_API_PAY_PREFIX = '/pay'
VITE_API_PAY_TARGET = 'https://huanda.xueai.art' # http://43.248.133.202
# 任务处理模块
VITE_API_WORKFLOW_UPLOAD = 'https://huanda.xueai.art/huandaCallback/workflow/file/upload' # https://sxwz.xueai.art/workflow https://designtools.xueai.art/workflow
VITE_API_WORKFLOW_WS = 'wss://huanda.xueai.art/task'
# 是否开启KKFileView
FILE_OPEN_PREVIEW = false
# KKFileView服务器地址
# FILE_VIEW_SERVER_URL = 'http://192.168.122.209:8012'

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

92
auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,92 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const ElMessage: typeof import('element-plus/es').ElMessage
const ElNotification: typeof import('element-plus/es').ElNotification
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const createPinia: typeof import('pinia').createPinia
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const defineStore: typeof import('pinia').defineStore
const effectScope: typeof import('vue').effectScope
const getActivePinia: typeof import('pinia').getActivePinia
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const mapActions: typeof import('pinia').mapActions
const mapGetters: typeof import('pinia').mapGetters
const mapState: typeof import('pinia').mapState
const mapStores: typeof import('pinia').mapStores
const mapWritableState: typeof import('pinia').mapWritableState
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const setActivePinia: typeof import('pinia').setActivePinia
const setMapStoreSuffix: typeof import('pinia').setMapStoreSuffix
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const storeToRefs: typeof import('pinia').storeToRefs
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useId: typeof import('vue').useId
const useLink: typeof import('vue-router').useLink
const useModel: typeof import('vue').useModel
const useRoute: typeof import('vue-router').useRoute
const useRouter: typeof import('vue-router').useRouter
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

75
components.d.ts vendored Normal file
View File

@ -0,0 +1,75 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AIgenerate: typeof import('./src/components/AIgenerate/AIgenerate.vue')['default']
Collection: typeof import('./src/components/collection/index.vue')['default']
copy: typeof import('./src/components/ModelDescription copy.vue')['default']
DeepseekPopover: typeof import('./src/components/AIgenerate/DeepseekPopover.vue')['default']
DialogBox: typeof import('./src/components/dialogBox/index.vue')['default']
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAside: typeof import('element-plus/es')['ElAside']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElMain: typeof import('element-plus/es')['ElMain']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElUpload: typeof import('element-plus/es')['ElUpload']
IEpArrow: typeof import('~icons/ep/arrow')['default']
IEpArrowDown: typeof import('~icons/ep/arrow-down')['default']
IEpBack: typeof import('~icons/ep/back')['default']
IEpCirclePlusFilled: typeof import('~icons/ep/circle-plus-filled')['default']
IEpClose: typeof import('~icons/ep/close')['default']
IEpDelete: typeof import('~icons/ep/delete')['default']
IEpDownload: typeof import('~icons/ep/download')['default']
IEpEdit: typeof import('~icons/ep/edit')['default']
IEpHouse: typeof import('~icons/ep/house')['default']
IEpLoading: typeof import('~icons/ep/loading')['default']
IEpPlus: typeof import('~icons/ep/plus')['default']
IEpRefresh: typeof import('~icons/ep/refresh')['default']
IEpRefreshRight: typeof import('~icons/ep/refresh-right')['default']
IEpRight: typeof import('~icons/ep/right')['default']
IEpStarFilled: typeof import('~icons/ep/star-filled')['default']
IEpTop: typeof import('~icons/ep/top')['default']
IEpUploadFilled: typeof import('~icons/ep/upload-filled')['default']
Img: typeof import('./src/components/Img/index.vue')['default']
Library: typeof import('./src/components/Library/index.vue')['default']
Model: typeof import('./src/components/dialogBox/model/index.vue')['default']
ModelParam: typeof import('./src/components/AIgenerate/components/modelParam.vue')['default']
Official: typeof import('./src/components/Library/official.vue')['default']
Private: typeof import('./src/components/Library/private.vue')['default']
Proportion: typeof import('./src/components/dialogBox/proportion/index.vue')['default']
Quantity: typeof import('./src/components/dialogBox/quantity/index.vue')['default']
Recommend: typeof import('./src/components/AIgenerate/components/recommend.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Select: typeof import('./src/components/Select/index.vue')['default']
Subpage: typeof import('./src/components/subpage.vue')['default']
UploadPicture: typeof import('./src/components/UploadPicture.vue')['default']
}
}

19
config/plugins.js Normal file
View File

@ -0,0 +1,19 @@
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export const autoImportConfig = AutoImport({
imports: [
'vue',
'vue-router',
'pinia'
],
dts: './auto-imports.d.ts',
resolvers: [ElementPlusResolver()]
})
export const componentsConfig = Components({
dts: './components.d.ts',
dirs: ['src/components'],
resolvers: [ElementPlusResolver()]
})

55
eslint.config.js Normal file
View File

@ -0,0 +1,55 @@
import antfu from '@antfu/eslint-config'
// https://github.com/antfu/eslint-config
export default antfu(
{
vue: true,
typescript: false,
ignores: [
'README.md',
'src/types/shims-vue.d.ts'
]
},
{
// Remember to specify the file glob here, otherwise it might cause the vue plugin to handle non-vue files
files: ['**/*.vue'],
rules: {
'vue/block-order': [2, {
order: [['script', 'template'], 'style']
}], // 强制组件顶级元素的顺序
'vue/html-self-closing': [0, {
html: {
void: 'never',
normal: 'always',
component: 'never'
}
}], // 强制自结束样式
'vue/custom-event-name-casing': [2, 'kebab-case'], // 对自定义事件名称强制使用特定大小写
'vue/singleline-html-element-content-newline': 0, // 要求在单行元素的内容前后换行
'vue/first-attribute-linebreak': 0, // 强制第一个属性的位置
'vue/define-macros-order': [2, {
order: ['defineOptions', 'defineModel', 'defineProps', 'defineEmits', 'defineSlots'],
defineExposeLast: false
}], // 强制执行定义限制和定义弹出编译器宏的顺序
'vue/html-indent': 0, // 在《模板》中强制一致的缩进
'vue/html-closing-bracket-newline': 0 // 要求或不允许在标记的右括号前换行
}
},
{
// Without `files`, they are general rules for all files
rules: {
'curly': [0, 'all'], // 对所有控制语句强制使用一致的大括号样式
'dot-notation': 0, // 尽可能强制使用点表示法。 在 JavaScript 中,可以使用点表示法 (foo.bar) 或方括号表示法 (foo["bar"]) 访问属性
'no-new': 0, // 不允许在赋值或比较之外使用 new 运算符
'no-console': 'off', // 禁止使用 console
'no-process-env': 0,
'style/arrow-parens': [2, 'always'], // 箭头函数参数需要括号
'style/brace-style': [2, '1tbs', { allowSingleLine: true }], // 对块执行一致的大括号样式
'style/comma-dangle': [2, 'never'], // 要求或不允许尾随逗号
'node/prefer-global/process': 0,
'quotes': [2, 'single', { avoidEscape: true }],
'antfu/top-level-function': 0,
'antfu/if-newline': 0
}
}
)

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Painting</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

140
out.txt Normal file
View File

@ -0,0 +1,140 @@
2026-02-04 11:48:53 INFO [XNIO-1 task-2] t.c.starter.log.interceptor.handler.LogInterceptor - [0][1171053593383952384] [POST] /business/collect
2026-02-04 11:48:53 ERROR [XNIO-1 task-2] t.c.s.w.a.r.DefaultBeforeControllerAdviceProcessImpl - [0][1171053593383952384] [POST] /business/collect
org.springframework.dao.DataIntegrityViolationException:
### Error updating database. Cause: java.sql.SQLException: Field 'create_user' doesn't have a default value
### The error may exist in top/continew/admin/business/mapper/CollectMapper.java (best guess)
### The error may involve top.continew.admin.business.mapper.CollectMapper.insert-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO t_collect ( user_id, url, clothes_id ) VALUES ( ?, ?, ? )
### Cause: java.sql.SQLException: Field 'create_user' doesn't have a default value
; Field 'create_user' doesn't have a default value
at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:258)
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:107)
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:92)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:439)
at jdk.proxy2/jdk.proxy2.$Proxy153.insert(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.insert(SqlSessionTemplate.java:272)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:59)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy$PlainMethodInvoker.invoke(MybatisMapperProxy.java:152)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:89)
at jdk.proxy2/jdk.proxy2.$Proxy191.insert(Unknown Source)
at top.continew.starter.extension.crud.service.impl.BaseServiceImpl.add(BaseServiceImpl.java:156)
at jdk.internal.reflect.GeneratedMethodAccessor528.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:354)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:392)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:720)
at top.continew.admin.business.service.impl.CollectServiceImpl$$SpringCGLIB$$0.add(<generated>)
at top.continew.admin.controller.business.CollectController.addCollect(CollectController.java:52)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:547)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614)
at io.undertow.servlet.handlers.ServletHandler.handleRequest(ServletHandler.java:74)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:129)
at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:91)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at top.continew.starter.log.interceptor.handler.LogFilter.doFilterInternal(LogFilter.java:82)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at cn.dev33.satoken.filter.SaPathCheckFilterForJakartaServlet.doFilter(SaPathCheckFilterForJakartaServlet.java:55)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:107)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at top.continew.starter.web.autoconfigure.trace.TLogServletFilter.doFilter(TLogServletFilter.java:59)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:67)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
at io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
at io.undertow.servlet.handlers.RedirectDirHandler.handleRequest(RedirectDirHandler.java:68)
at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:117)
at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.SendErrorPageHandler.handleRequest(SendErrorPageHandler.java:52)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:276)
at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135)
at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:132)
at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:256)
at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:101)
at io.undertow.server.Connectors.executeRootHandler(Connectors.java:393)
at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:859)
at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
at org.xnio.XnioWorker$WorkerThreadFactory$1$1.run(XnioWorker.java:1282)
at java.base/java.lang.Thread.run(Thread.java:840)
Caused by: java.sql.SQLException: Field 'create_user' doesn't have a default value
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:130)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:912)
at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:354)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.execute(ProxyPreparedStatement.java:44)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.execute(HikariProxyPreparedStatement.java)
at com.p6spy.engine.wrapper.PreparedStatementWrapper.execute(PreparedStatementWrapper.java:362)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(PreparedStatementHandler.java:48)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.update(RoutingStatementHandler.java:75)
at jdk.internal.reflect.GeneratedMethodAccessor215.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
at jdk.proxy2/jdk.proxy2.$Proxy260.update(Unknown Source)
at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:50)
at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117)
at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76)
at jdk.internal.reflect.GeneratedMethodAccessor214.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.apache.ibatis.plugin.Invocation.proceed(Invocation.java:61)
at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:106)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:59)
at jdk.proxy2/jdk.proxy2.$Proxy258.update(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:197)
at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:184)
at jdk.internal.reflect.GeneratedMethodAccessor221.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:425)
... 93 common frames omitted
2026-02-04 11:48:53 INFO [XNIO-1 task-2] t.c.starter.log.interceptor.handler.LogInterceptor - [0][1171053593383952384] [POST] /business/collect 200 3ms

27
package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "ai-painting-v2.0",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.4",
"eslint": "^10.0.3",
"less": "^4.5.1",
"unplugin-auto-import": "^21.0.0",
"unplugin-icons": "^23.0.1",
"unplugin-vue-components": "^31.0.0",
"vite": "^7.3.1"
},
"dependencies": {
"axios": "^1.13.6",
"element-plus": "^2.13.5",
"pinia": "^3.0.4",
"vue": "^3.5.29",
"vue-element-plus-x": "^1.3.98",
"vue-router": "^5.0.3",
"vue-virtual-scroller": "2.0.0-beta.8"
}
}

4673
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

54
src/App.vue Normal file
View File

@ -0,0 +1,54 @@
<script setup>
defineOptions({ name: 'App' })
//
const adjustZoomBasedOnSystemScale = () => {
//
const systemScale = window.devicePixelRatio
console.log(`系统缩放比例: ${systemScale}`)
//
if (systemScale === 1.25) { // 125%
// 80% (1 / 1.25)
document.body.style.transform = 'scale(0.8)'
document.body.style.transformOrigin = 'left top'
document.body.style.width = '125%'
document.body.style.height = '125vh'
}
}
//
onMounted(() => {
//
adjustZoomBasedOnSystemScale()
//
window.addEventListener('resize', adjustZoomBasedOnSystemScale)
// ()
if ('onpageshow' in window) {
window.addEventListener('pageshow', adjustZoomBasedOnSystemScale)
}
})
onUnmounted(() => {
//
window.removeEventListener('resize', adjustZoomBasedOnSystemScale)
if ('onpageshow' in window) {
window.removeEventListener('pageshow', adjustZoomBasedOnSystemScale)
}
})
</script>
<template>
<router-view />
</template>
<style lang="less" scoped>
/* 滚动条样式 */
::-webkit-scrollbar {
width: 0;
height: 0;
}
</style>

51
src/apis/auth/auth.js Normal file
View File

@ -0,0 +1,51 @@
import service from '@/utils/request'
const BASE_URL = '/auth'
/** @desc 账号登录 */
export function accountLogin(req) {
return service.post(`${BASE_URL}/account`, req)
}
// 获取邀请码
export function getCodePhone(params) {
return service.post(`${BASE_URL}/register/phone`, params)
}
/** @desc 手机号登录 */
export function phoneLogin(req) {
return service.post(`${BASE_URL}/phone`, req)
}
/** @desc 邮箱登录 */
export function emailLogin(req) {
return service.post(`${BASE_URL}/email`, req)
}
/** @desc 三方账号登录 */
export function socialLogin(source, req) {
return service.post(`/oauth/${source}`, req)
}
/** @desc 三方账号登录授权 */
export function socialAuth(source) {
return service.get(`/oauth/${source}`)
}
/** @desc 退出登录 */
export function logout() {
return service.post(`${BASE_URL}/logout`)
}
/** @desc 获取用户信息 */
export const getUserInfo = () => {
return service.get(`${BASE_URL}/user/info`)
}
/** @desc 获取路由信息 */
export const getUserRoute = () => {
return service.get(`${BASE_URL}/route`)
}
export const checkUsertoken = () => {
return service.get(`${BASE_URL}/check/token`)
}

32
src/apis/auth/captcha.js Normal file
View File

@ -0,0 +1,32 @@
import service from '@/utils/request'
const BASE_URL = '/captcha'
/** @desc 获取图片验证码 */
export function getImageCaptcha() {
return service.get(`${BASE_URL}/image`)
}
// **获取短信
export function getSmsCaptchaBin(phone) {
return service.get(`${BASE_URL}/sms?phone=${phone}`)
}
/** @desc 获取短信验证码 */
export function getSmsCaptcha(phone, captchaReq) {
return service.get(`${BASE_URL}/sms?phone=${phone}&captchaVerification=${encodeURIComponent(captchaReq.captchaVerification || '')}`)
}
/** @desc 获取邮箱验证码 */
export function getEmailCaptcha(email, captchaReq) {
return service.get(`${BASE_URL}/mail?email=${email}&captchaVerification=${encodeURIComponent(captchaReq.captchaVerification || '')}`)
}
/** @desc 获取行为验证码 */
export function getBehaviorCaptcha(req) {
return service.get(`${BASE_URL}/behavior`, req)
}
/** @desc 校验行为验证码 */
export function checkBehaviorCaptcha(req) {
return service.post(`${BASE_URL}/behavior`, req)
}

2
src/apis/auth/index.js Normal file
View File

@ -0,0 +1,2 @@
export * from './auth.js'
export * from './captcha.js'

View File

@ -0,0 +1,18 @@
import service from '@/utils/request'
const BASE_URL = '/business'
/** @desc 传入userId、url、clothesId */
export function setCollectImg(req) {
return service.post(`${BASE_URL}/collect`, req)
}
/** @desc 根据id批量取消收藏 */
export function delCollectImg(req) {
return service.del(`${BASE_URL}/collect/batchDelete`, req)
}
/** @desc 根据userId分页查询收藏 */
export function getCollectImg(req) {
return service.get(`${BASE_URL}/collect/page`, req)
}

View File

@ -0,0 +1,18 @@
import service from '@/utils/request'
const BASE_URL = '/business'
/** @desc 通过rootid查询包含的任务列表 */
export function setTaskRootId(req) {
return service.post(`${BASE_URL}/rootTask`, req)
}
/** @desc 查询用户所含的rootId */
export function AllRootTask(req) {
return service.get(`${BASE_URL}/rootTask/page`, { params: req })
}
/** @desc 查询用户所含的rootId */
// export function getLibraryImg(req) {
// return service.put(`${BASE_URL}/rootTask/${req}`)
// }

13
src/apis/history/index.js Normal file
View File

@ -0,0 +1,13 @@
import service from '@/utils/request'
const BASE_URL = '/billing'
/** @desc 通过rootid查询包含的任务列表 */
export function getTaskListByRootId(req) {
return service.post(`${BASE_URL}/queryTaskListByRootId`, req)
}
/** @desc 查询用户所含的rootId */
export function getLibraryImg(req) {
return service.get(`${BASE_URL}/page`, { params: req })
}

18
src/apis/library/index.js Normal file
View File

@ -0,0 +1,18 @@
import service from '@/utils/request'
const BASE_URL = '/business/huandaResource'
/** @desc 添加图片 */
export function addLibraryImg(req) {
return service.post(`${BASE_URL}`, req)
}
/** @desc 删除图片 */
export function delLibraryImg(req) {
return service.post(`${BASE_URL}/batchDelete`, req)
}
/** @desc 查询单个库的所有图片 */
export function getLibraryImg(req) {
return service.get(`${BASE_URL}/page`, { params: req })
}

28
src/apis/pay/aliPay.js Normal file
View File

@ -0,0 +1,28 @@
// axios 发送ajax请求
import service from '@/utils/request'
export default {
// 发起支付请求
tradePagePay(productId, userId, totalFee) {
return service({
url: `/pay/ali-pay/trade/page/pay/${productId}`,
params: { userId, totalFee },
method: 'post'
})
},
cancel(orderNo) {
return service({
url: `/pay/ali-pay/trade/close/${orderNo}`,
method: 'post'
})
},
refunds(orderNo, reason) {
return service({
url: `/pay/ali-pay/trade/refund/${orderNo}/${reason}`,
method: 'post'
})
}
}

11
src/apis/pay/index.js Normal file
View File

@ -0,0 +1,11 @@
import aliPayApi from './aliPay'
import orderInfoApi from './orderInfo'
import productApi from './product'
import wxPayApi from './wxPay'
export {
aliPayApi,
orderInfoApi,
productApi,
wxPayApi
}

27
src/apis/pay/orderInfo.js Normal file
View File

@ -0,0 +1,27 @@
import request from '@/utils/request'
export default {
// 查询订单列表
list() {
return request({
url: '/apy/order-info/list',
method: 'get'
})
},
// 查询订单状态
queryOrderStatus(orderNo) {
return request({
url: `/pay/order-info/query-order-status/${orderNo}`,
method: 'get'
})
},
// 查询用户的sysBeans
queryUserSysBeans(userId) {
return request({
url: `/pay/beans/getSysBeans/${userId}`,
method: 'get'
})
}
}

13
src/apis/pay/product.js Normal file
View File

@ -0,0 +1,13 @@
// axios 发送ajax请求
import request from '@/utils/request'
export default {
// 查询商品列表(对远程接口调用)
list() {
return request({
url: '/pay/product/list',
method: 'get'
})
}
}

36
src/apis/pay/wxPay.js Normal file
View File

@ -0,0 +1,36 @@
// axios 发送ajax请求
import request from '@/utils/request'
export default {
// Native下单
nativePay(productId, userId, totalFee) {
return request({
url: `/pay/wx-pay/native/${productId}`,
params: { userId, totalFee },
method: 'post'
})
},
// Native下单(v2)
nativePayV2(productId) {
return request({
url: `/pay/wx-pay-v2/native/${productId}`,
method: 'post'
})
},
cancel(orderNo) {
return request({
url: `/pay/wx-pay/cancel/${orderNo}`,
method: 'post'
})
},
refunds(orderNo, reason) {
return request({
url: `/pay/wx-pay/refunds/${orderNo}/${reason}`,
method: 'post'
})
}
}

107
src/apis/workflows.json Normal file
View File

@ -0,0 +1,107 @@
{
"model": {
"workflowId": "1998946903104610306",
"nodeInfoList": [
{
"nodeId": "12",
"fieldName": "text",
"fieldValue": "一位中年女医生,短发利落清爽,身着薄荷绿医务衬衫。她以站立的姿势,面向镜头。背景为柔和的白色眼神明亮而充满鼓励。"
},
{
"nodeId": "16",
"fieldName": "aspect_ratio",
"fieldValue": "aspect_ratio"
}
]
},
"background_pose": {
"workflowId": "1996842562587611138",
"nodeInfoList": [
{
"nodeId": "11",
"fieldName": "text",
"fieldValue": ""
},
{
"nodeId": "10",
"fieldName": "aspect_ratio",
"fieldValue": "aspect_ratio"
}
]
},
"huanda": {
"workflowId": "1996836769582784513",
"nodeInfoList": [
{
"nodeId": "2",
"fieldName": "image",
"fieldValue": "image"
},
{
"nodeId": "3",
"fieldName": "image",
"fieldValue": "image"
},
{
"nodeId": "10",
"fieldName": "image",
"fieldValue": "image"
},
{
"nodeId": "14",
"fieldName": "image",
"fieldValue": "image"
},
{
"nodeId": "18",
"fieldName": "index",
"fieldValue": "index"
},
{
"nodeId": "34",
"fieldName": "index",
"fieldValue": "index"
},
{
"nodeId": "37",
"fieldName": "index",
"fieldValue": "index"
},
{
"nodeId": "29",
"fieldName": "aspect_ratio",
"fieldValue": "aspect_ratio"
}
]
},
"video": {
"workflowId": "1996136037531500545",
"nodeInfoList": [
{
"nodeId": "55",
"fieldName": "prompt",
"fieldValue": ""
},
{
"nodeId": "16",
"fieldName": "image",
"fieldValue": "image"
}
]
},
"talk": {
"workflowId": "1996137696278069250",
"nodeInfoList": [
{
"nodeId": "1",
"fieldName": "prompt",
"fieldValue": ""
},
{
"nodeId": "2",
"fieldName": "image",
"fieldValue": "image"
}
]
}
}

View File

@ -0,0 +1,3 @@
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 1L5 10V12.25V13M5 1L9 4.75M5 1L1 4.75" stroke="#000F33" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -0,0 +1,5 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.5" y="2.5" width="15" height="13" rx="1.5" stroke="#333333"/>
<path d="M1.5 13L5.5 9L7.5 11L10 8L16.5 13" stroke="#333333" stroke-linejoin="round"/>
<circle cx="6" cy="6" r="1" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.5 2H2V5.5M2 12.5V16H5.5M12.5 16H16V12.5M16 5.5V2H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 12H4L6.5 6.8L9 9.2L11.0833 6L14 12Z" stroke="#333333" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 329 B

View File

@ -0,0 +1,3 @@
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 1L5 10V12.25V13M5 1L9 4.75M5 1L1 4.75" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

View File

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 3L5 7L9 3" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@ -0,0 +1,3 @@
<svg width="15" height="10" viewBox="0 0 15 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.75 4.75H10.5H12.9375H13.75M0.75 4.75L4.8125 0.75M0.75 4.75L4.8125 8.75" stroke="#000F33" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 272 B

View File

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.32715 11.7025C6.00568 10.5268 7.5084 10.1244 8.68457 10.805L8.68555 10.806C9.85618 11.4798 10.2587 12.9731 9.58984 14.1478C9.26516 14.6394 8.92437 14.8919 8.5791 15.0423C8.21163 15.2025 7.82213 15.2583 7.35254 15.3285C6.90422 15.3954 6.37916 15.4761 5.87402 15.723C5.61921 15.8476 5.37578 16.0103 5.14746 16.2249C5.10088 15.874 5.05767 15.4538 5.0293 15.0072C4.98791 14.3556 4.97855 13.6613 5.02637 13.0511C5.07582 12.4204 5.1825 11.9531 5.32715 11.7025Z" stroke="white"/>
<path d="M13.1123 1.51855C13.2251 1.36723 13.4702 1.30076 13.6914 1.42871L13.6934 1.42969C13.9035 1.55061 13.9744 1.77925 13.9141 1.95117L11.1045 9.22266C11.0794 9.28753 10.9287 9.43553 10.5088 9.48242C10.1219 9.52553 9.73113 9.4495 9.53125 9.33398H9.53027C9.30022 9.20088 8.99283 8.8614 8.79785 8.46484C8.70338 8.27267 8.64996 8.09573 8.63672 7.9541C8.62357 7.81331 8.65211 7.74376 8.6748 7.71191L13.1123 1.51855Z" stroke="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1021 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.1494 2.5C13.039 2.5 14.4998 3.96763 14.5 5.88477C14.5 7.06494 13.9819 8.17537 12.9678 9.4209C11.9479 10.6736 10.4752 12.006 8.64648 13.6387C8.63893 13.6454 8.63116 13.652 8.62402 13.6592L8.35547 13.9307C8.15984 14.1281 7.84016 14.1281 7.64453 13.9307L7.37598 13.6592L7.35352 13.6387L6.05078 12.4668C4.81731 11.3444 3.79719 10.3604 3.03223 9.4209C2.01813 8.17537 1.5 7.06494 1.5 5.88477C1.50024 3.9673 2.9594 2.5 4.84863 2.5C5.88613 2.5 6.93824 2.99756 7.61523 3.80469C7.71024 3.91796 7.85021 3.9834 7.99805 3.9834C8.14588 3.9834 8.28586 3.91796 8.38086 3.80469C9.05777 2.99766 10.1098 2.5 11.1494 2.5Z" stroke="white" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 760 B

View File

@ -0,0 +1,3 @@
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 8.00781C12.6133 8.00466 12.7248 8.04356 12.8145 8.11816C12.904 8.19271 12.9665 8.29836 12.9922 8.41699L13 8.50684V12.6689C12.9999 13.3819 12.389 13.9322 11.6416 13.9941L11.5 14H1.5C0.738674 14 0.0822447 13.4908 0.0078125 12.8008L0 12.6689V8.50684C0.000122217 8.23129 0.22429 8.00781 0.5 8.00781C0.613183 8.00476 0.723919 8.04365 0.813477 8.11816C0.903119 8.19275 0.966547 8.29825 0.992188 8.41699L1 8.50684V12.6689C1.00018 12.8127 1.16305 12.9632 1.40527 12.9951L1.5 13.001H11.5C11.7609 13.001 11.9531 12.8665 11.9922 12.7217L12 12.6689V8.50684C12.0001 8.23144 12.2236 8.00804 12.5 8.00781ZM6.40723 0.000976562C6.55495 0.00100048 6.69632 0.0644185 6.80078 0.176758C6.90527 0.289135 6.96387 0.44166 6.96387 0.600586V8.30957L8.60156 6.5498C8.65315 6.49348 8.71447 6.44864 8.78223 6.41797C8.84994 6.38733 8.92263 6.3714 8.99609 6.37109C9.06964 6.37082 9.14295 6.38585 9.21094 6.41602C9.27888 6.44618 9.34063 6.49094 9.39258 6.54688C9.44447 6.6028 9.48574 6.66911 9.51367 6.74219C9.54161 6.81528 9.55598 6.89363 9.55566 6.97266C9.55532 7.05175 9.54031 7.13026 9.51172 7.20312C9.48312 7.276 9.44113 7.34202 9.38867 7.39746L6.89355 10.0811C6.82386 10.1561 6.73704 10.2105 6.6416 10.2373L6.54785 10.2539H6.45312C6.32235 10.2422 6.19927 10.1808 6.10645 10.0811L3.61133 7.39746C3.50818 7.28484 3.45066 7.13269 3.45117 6.97461C3.45175 6.81663 3.51045 6.66549 3.61426 6.55371C3.71812 6.44189 3.8589 6.37866 4.00586 6.37793C4.15272 6.37729 4.29368 6.43908 4.39844 6.5498L5.84961 8.10938L5.85059 0.599609C5.85059 0.440856 5.9094 0.288124 6.01367 0.175781C6.11816 0.0634039 6.26044 0 6.4082 0L6.40723 0.000976562Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,178 @@
<template>
<div class="img-container">
<img
:src="props.src"
:alt="props.alt"
class="img-element"
@click="handleImageClick"
/>
<div class="fullscreen-icon" @click="toggleFullscreen">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 14H5V19H10V17H7V14Z" fill="currentColor" />
<path d="M5 10H7V7H10V5H5V10Z" fill="currentColor" />
<path d="M17 17H14V19H19V14H17V17Z" fill="currentColor" />
<path d="M14 5V7H17V10H19V5H14Z" fill="currentColor" />
</svg>
</div>
<Teleport to="body">
<Transition name="fade">
<div v-if="isFullscreen" class="fullscreen-overlay" @click="closeFullscreen">
<img :src="props.src" :alt="props.alt" class="fullscreen-image" @click.stop />
<button class="close-button" @click="closeFullscreen">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</div>
</Transition>
</Teleport>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
const props = defineProps({
src: {
type: String,
required: true
},
alt: {
type: String,
default: ''
}
})
const isFullscreen = ref(false)
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value
}
const closeFullscreen = () => {
isFullscreen.value = false
}
const handleImageClick = () => {
isFullscreen.value = true
}
const handleKeydown = (e) => {
if (e.key === 'Escape' && isFullscreen.value) {
closeFullscreen()
}
}
onMounted(() => {
window.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown)
})
</script>
<style scoped>
.img-container {
position: relative;
display: inline-block;
overflow: hidden;
border-radius: 5px;
width: 100%;
max-height: 100%;
}
.img-element {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
cursor: pointer;
transition: transform 0.3s ease;
}
.img-element:hover {
transform: scale(1.02);
}
.fullscreen-icon {
position: absolute;
top: 8px;
right: 8px;
width: 32px;
height: 32px;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
color: white;
}
.img-container:hover .fullscreen-icon {
opacity: 1;
}
.fullscreen-icon:hover {
background-color: rgba(0, 0, 0, 0.8);
}
.fullscreen-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 40px;
}
.fullscreen-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
cursor: default;
}
.close-button {
position: absolute;
top: 20px;
right: 20px;
width: 48px;
height: 48px;
background-color: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: white;
transition: all 0.3s ease;
}
.close-button:hover {
background-color: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
transform: scale(1.1);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,274 @@
<template>
<div class="custom-select" :class="props.class">
<div
ref="headerRef"
class="select-header"
:class="{ open: isOpen }"
:style="{ width: headerWidth }"
@click.stop="toggleDropdown"
>
<slot name="prefix" />
<span class="select-text">{{ selectedOption || placeholder }}</span>
<div v-if="props.isArrow" class="arrow-icon" :class="{ rotate: isOpen }"><i-ep-ArrowDown /></div>
</div>
<div
v-if="isOpen"
ref="menuRef"
class="dropdown-menu"
:class="position"
>
<div
v-for="(option, index) in options"
:key="option.value || index"
class="dropdown-item"
:class="{ selected: option.value === selectedValue }"
@click.stop="selectOption(option)"
>
<slot name="option" :option="option" :selected="option.value === selectedValue">
{{ option.label }}
</slot>
</div>
</div>
</div>
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number],
default: ''
},
isArrow: {
type: Boolean,
default: true
},
options: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: '请选择'
},
position: {
type: String,
default: 'bottom'
},
width: {
type: [String, Number],
default: 'auto'
},
background: {
type: String,
default: '#ffffff'
},
class: {
type: [String, Object, Array],
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const selectId = ref(Math.random().toString(36).substr(2, 9))
const isOpen = ref(false)
const selectedValue = ref(props.modelValue)
const headerRef = ref(null)
const menuRef = ref(null)
const menuWidth = ref('100px')
if (!window.__currentOpenSelectId__) {
window.__currentOpenSelectId__ = null
}
const headerWidth = computed(() => {
if (typeof props.width === 'number') {
return `${props.width}px`
}
return props.width
})
const selectedOption = computed(() => {
const option = props.options.find((opt) => opt.value === selectedValue.value)
return option ? option.label : ''
})
const toggleDropdown = async () => {
if (isOpen.value) {
isOpen.value = false
window.__currentOpenSelectId__ = null
} else {
if (window.__currentOpenSelectId__ && window.__currentOpenSelectId__ !== selectId.value) {
window.dispatchEvent(new CustomEvent('close-other-selects', { detail: { excludeId: selectId.value } }))
}
isOpen.value = true
window.__currentOpenSelectId__ = selectId.value
await nextTick()
if (headerRef.value) {
menuWidth.value = `${headerRef.value.offsetWidth}px`
}
}
}
const selectOption = (option) => {
selectedValue.value = option.value
emit('update:modelValue', option.value)
isOpen.value = false
window.__currentOpenSelectId__ = null
}
watch(() => props.modelValue, (newValue) => {
selectedValue.value = newValue
})
const handleClickOutside = (e) => {
if (menuRef.value && !menuRef.value.contains(e.target) && headerRef.value && !headerRef.value.contains(e.target)) {
isOpen.value = false
window.__currentOpenSelectId__ = null
}
}
const handleCloseOtherSelects = (e) => {
if (e.detail.excludeId !== selectId.value) {
isOpen.value = false
}
}
document.addEventListener('click', handleClickOutside)
window.addEventListener('close-other-selects', handleCloseOtherSelects)
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
window.removeEventListener('close-other-selects', handleCloseOtherSelects)
})
</script>
<style scoped>
.custom-select {
position: relative;
display: inline-block;
}
.select-header {
width: v-bind(width);
height: 36px;
background-color: v-bind(background);
border-radius: 10px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 15px;
cursor: pointer;
transition: all 0.3s ease;
gap: 8px;
}
.select-header:hover {
background-color: #E9EBEF;
}
.select-header :deep(.prefix-slot) {
display: flex;
align-items: center;
flex-shrink: 0;
}
.select-text {
font-family: 'Microsoft YaHei';
font-size: 14px;
color: #333333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.arrow-icon {
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
font-size: 12px;
color: #666666;
}
.arrow-icon.rotate {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
background-color: #FFFFFF;
border-radius: 20px;
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.1);
padding: 20px 0;
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
left: 50%;
transform: translateX(-50%);
}
.dropdown-menu.bottom {
top: 100%;
left: 50%;
transform: translateX(-50%);
margin-top: 8px;
}
.dropdown-menu.top {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 8px;
}
.dropdown-menu.left {
right: 100%;
top: 0;
margin-left: 8px;
}
.dropdown-menu.right {
left: 100%;
top: 0;
margin-right: 8px;
}
.dropdown-item {
font-family: 'Microsoft YaHei';
font-size: 14px;
color: #666666;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
padding: 0 20px;
min-width: 80px;
text-align: center;
}
.dropdown-item:hover {
color: #000F33;
}
.dropdown-item.selected {
color: #000F33;
font-weight: 400;
}
.dropdown-item:not(:last-child)::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 28px;
height: 1px;
background-color: rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -0,0 +1,150 @@
<template>
<el-upload
ref="uploadRef"
class="uploader"
:action="uploadurl"
multiple
:limit="props.uploadNumber"
list-type="picture-card"
:before-upload="beforeUpload"
:on-success="SuccessSet"
:on-remove="Remove"
:on-exceed="handleExceed"
:on-error="Errors"
:class="{ exceed: imageNum === 6 }"
>
<div class="uploader-icon"><i-ep-plus /></div>
</el-upload>
<!-- 模特库组件 -->
<div>
<Library v-if="useDisplay[props.type].Library" :type="props.type" @select="handleSelect">
<template #filters>
<slot name="filters" />
</template>
<template #create-settings>
<slot name="create-settings" />
</template>
</Library>
<collection v-if="useDisplay[props.type].collection" :type="props.type" @select="handleSelect" />
</div>
</template>
<script setup>
import { genFileId } from 'element-plus'
import Library from '@/components/Library/index.vue'
import { useDisplayStore, useParamStore } from '@/stores'
const props = defineProps({
type: {
type: String,
default: ''
},
uploadNumber: {
type: Number,
default: 1
}
})
const uploadurl = import.meta.env.VITE_API_WORKFLOW_UPLOAD
const imageNum = ref(0)
const images = ref([])
const useParams = useParamStore()
const uploadRef = ref(null)
const useDisplay = useDisplayStore()
const handleSelect = async (url) => {
// URLBlob
const initialFile = await fetch(url)
const blob = await initialFile.blob()
if (props.type === 'clothes') useParams.rootTaskImage = url
//
const file = new File([blob], `selected_image_${Date.now()}.jpg`, { type: blob.type })
file.uid = genFileId()
console.log('file', file)
if (props.uploadNumber === 1 && imageNum.value === 1) uploadRef.value.clearFiles() // 1
if (imageNum.value >= 6) {
// eslint-disable-next-line no-undef
ElMessage.warning('图片数量已达到上限,请删除图片后重新上传')
return
}
//
uploadRef.value.handleStart(file)
uploadRef.value.submit()
}
//
const beforeUpload = (rawFile) => {
console.log('beforeUpload', rawFile)
const allowedTypes = ['image/jpeg', 'image/png']
//
if (!allowedTypes.includes(rawFile.type)) {
// eslint-disable-next-line no-undef
ElMessage.error('不支持的文件类型,请上传 JPG、PNG 格式的图片')
return false
}
// 2MB
if (rawFile.size / 1024 / 1024 > 10) {
// eslint-disable-next-line no-undef
ElMessage.error('图片大小不能超过 10MB')
return false
}
}
// state
const SuccessSet = (response, uploadFile) => {
console.log('上传成功', response)
images.value.push({ id: uploadFile.uid, url: response.url })
useParams.setParams(response.url, props.type, true)
imageNum.value += 1
}
//
const handleExceed = (files) => {
console.log('超出限制', files[0])
if (props.uploadNumber === 1) {
uploadRef.value.clearFiles()
const file = files[0]
file.uid = genFileId()
uploadRef.value.handleStart(file)
uploadRef.value.submit()
imageNum.value = 0
return
}
// eslint-disable-next-line no-undef
ElMessage.error('姿势与背景最多只能上传六张图片')
}
//
const Errors = (error) => {
imageNum.value -= 1
console.log('上传失败', images.value)
console.log('上传失败', error)
// eslint-disable-next-line no-undef
ElMessage.error('上传失败请重新添加图片,并点击生成')
}
//
const Remove = (UploadFile) => {
console.log('Remove', UploadFile)
images.value = images.value.filter((item) => item !== UploadFile.uid)
imageNum.value -= 1
}
watch(() => useParams.AIimageUrl[props.type], (newValue) => {
if (newValue) {
handleSelect(newValue)
// store
useParams.AIimageUrl[props.type] = ''
}
})
</script>
<style scoped>
@import "@/styles/home/uploadImg.css";
</style>

View File

@ -0,0 +1,335 @@
<template>
<Transition name="slide-up">
<div class="input-container" :class="{ generate : props.isGenerate}">
<div v-if="props.isGenerate" class="title">AI绘画2026</div>
<Sender :key="useDisplay.Sender_variant" v-model="prompt" :variant="useDisplay.Sender_variant" :placeholder="promptPlaceholder" :submit-btn-disabled="isgerenate.value" :auto-size="autoSizeConfig">
<template #prefix>
<div v-show="useDisplay.Sender_variant !== 'default'" class="prefix-self-wrap">
<div class="upload-btn">
<img src="@/assets/dialog/originalImage.svg" alt="" style="width: 16px;">
<span>上传原图</span>
</div>
<div class="upload-btn">
<img src="@/assets/dialog/referenceDiagram.svg" alt="" style="width: 16px;">
<span>上传参考图</span>
</div>
<Model v-model="model" />
<Proportion v-model="proportion" />
<Quantity v-model="quantity" />
</div>
</template>
<template #action-list>
<div style="display: flex; align-items: center; gap: 8px; height: 100%;">
<el-button v-if="isgerenate" round color="#626aef" style="animation: spin 1s linear infinite;">
<!-- <i-ep-loading /> -->
</el-button>
<div v-else class="gerenate" :class="{ isprompt: prompt }" @click="generate">
<img v-if="!prompt" src="@/assets/dialog/darkArrow.svg" alt="" />
<img v-else src="@/assets/dialog/writerArrow.svg" alt="" />
<div v-show="useDisplay.Sender_variant !== 'default'">生成</div>
</div>
</div>
</template>
</Sender>
<el-upload
v-show="false"
ref="uploadRef"
class="uploader"
:action="uploadurl"
multiple
:limit="1"
list-type="picture-card"
:before-upload="beforeUpload"
:on-success="SuccessSet"
:on-error="Errors"
:class="{ exceed: imageurl }"
/>
</div>
</Transition>
</template>
<script setup>
import Proportion from './proportion/index.vue'
import Quantity from './quantity/index.vue'
import Model from './model/index.vue'
import { Sender } from 'vue-element-plus-x'
import { useDisplayStore, useParamStore } from '@/stores'
import { generateSubImage } from '@/utils/websocket'
const props = defineProps({
isGenerate: {
type: Boolean,
default: false
}
})
console.log(props.isGenerate)
const useDisplay = useDisplayStore()
const useParams = useParamStore()
const uploadurl = import.meta.env.VITE_API_WORKFLOW_UPLOAD
const uploadRef = ref(null)
const model = ref('flux')
const proportion = ref('9:16')
const quantity = ref(1)
const promptPlaceholder = '结合图片,描述你想生成的画面和动作。例如:电影感剧照,氛围温聲。柔和色调,略带胶片颗粒感,连贯摆出棚拍的动作。'
const prompt = ref('')
const imageurl = ref('')
const imageurlShow = ref('')
const isgerenate = ref(false)
const autoSizeConfig = computed(() => {
if (useDisplay.Sender_variant !== 'default') {
return { minRows: 3, maxRows: 3 }
} else {
return { minRows: 1, maxRows: 1 }
}
})
//
const handleSelect = async (url) => {
imageurlShow.value = url
// URLBlob
const initialFile = await fetch(url)
const blob = await initialFile.blob()
//
const file = new File([blob], `selected_image_${Date.now()}.jpg`, { type: blob.type })
file.uid = genFileId()
console.log('file', file)
uploadRef.value.clearFiles() // 1
//
uploadRef.value.handleStart(file)
uploadRef.value.submit()
}
//
const beforeUpload = (rawFile) => {
console.log('beforeUpload', rawFile)
const allowedTypes = ['image/jpeg', 'image/png']
//
if (!allowedTypes.includes(rawFile.type)) {
// eslint-disable-next-line no-undef
ElMessage.error('不支持的文件类型,请上传 JPG、PNG 格式的图片')
return false
}
// 2MB
if (rawFile.size / 1024 / 1024 > 10) {
// eslint-disable-next-line no-undef
ElMessage.error('图片大小不能超过 10MB')
return false
}
}
// state
const SuccessSet = (response) => {
console.log('上传成功', response)
// eslint-disable-next-line no-undef
ElMessage.success('上传成功')
imageurl.value = response.url
}
//
const Errors = (error) => {
console.log('上传失败', error)
// eslint-disable-next-line no-undef
ElMessage.error('上传失败,请重新选择图片')
imageurlShow.value = ''
}
const generate = async () => {
if (!imageurl.value && !prompt.value) {
// eslint-disable-next-line no-undef
ElMessage.error('请输入提示词')
return
}
isgerenate.value = true
useDisplay.isSubGerenate = true
console.log('生成开始', isgerenate.value)
await generateSubImage(3, { videoImg: imageurl.value, text: prompt.value, file_type: 'video', parentCreateTime: parentTime.value, parentIndex: parentIndex.value, parentTaskId: parentTaskId.value }, '生成视频')
console.log('生成中', isgerenate.value)
}
watch(() => useDisplay.isSubGerenate, (newValue) => {
console.log('生成状态', newValue)
if (!newValue) {
console.log('生成完成', isgerenate.value)
isgerenate.value = useDisplay.isSubGerenate
}
})
watch(() => useParams.AIvideoImage, (newValue) => {
if (newValue) {
if (newValue.url === imageurl.value) return
console.log('图片选择成功,打开视频页面', newValue)
parentTime.value = newValue.time
parentIndex.value = newValue.parentIndex
parentTaskId.value = newValue.parentTaskId
imageurl.value = newValue.url
handleSelect(newValue.url)
useParams.AIvideoImage = ''
}
})
</script>
<style lang="less" scoped>
/* 输入区域 */
.input-container {
width: 760px;
position: absolute;
bottom: 10px;
z-index: 100;
left: 50%;
transform: translateX(-50%);
border-radius: 10px;
}
.generate{
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
gap: 40px;
position: relative;
border: none;
box-shadow: none;
:deep(.el-sender){
background-color: #F8F9FA;
border: none;
box-shadow: none;
}
}
.prefix-self-wrap{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
img{
height: 50px;
border-radius: 4px;
}
}
.title{
background-color: #FFF;
color: #333;
text-align: center;
font-family: "Alibaba PuHuiTi";
font-size: 24px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
:deep(.el-sender){
background-color: #ffffff;
border: none;
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.10);
}
:deep(.el-sender:focus-within){
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.10);
}
//
.select{
background: #ffffff;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.10);
}
//
.upload-btn{
display: flex;
height: 40px;
padding: 0 15px;
justify-content: center;
align-items: center;
gap: 5px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.10);
background: #ffffff;
cursor: pointer;
position: relative;
}
.upload-btn:hover{
background: #E5E7EB;
}
/* 圆形按钮 */
.circle-btn {
position: absolute;
right: 0px;
top: 0px;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 90;
transition: all 0.3s ease;
color: rgb(0, 0, 0);
font-size: 20px;
&:hover {
transform: scale(1.1);
// box-shadow: 0 6px 16px rgba(98, 106, 239, 0.6);
}
&:active {
transform: scale(0.95);
}
}
/* 过渡动画 */
.slide-up-enter-active,
.slide-up-leave-active {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-up-enter-from {
opacity: 0;
transform: translate(-50%, 100%);
}
.slide-up-leave-to {
opacity: 0;
transform: translate(-50%, 100%);
}
.gerenate{
display: inline-flex;
height: 40px;
padding: 0 20px;
justify-content: center;
align-items: center;
gap: 5px;
border-radius: 10px;
background: rgba(0, 15, 51, 0.10);
cursor: pointer;
color: #000F33;
text-align: center;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: normal;
}
.isprompt{
color: #ffffff;
background-color: #000F33;
}
// .gerenate:hover{
// background: rgba(0, 15, 51, 0.20);
// }
</style>

View File

@ -0,0 +1,159 @@
<template>
<el-popover trigger="click" placement="top" :width="180">
<div class="select">
<div class="model-group">
<div class="group-title">生成模型</div>
<div
v-for="item in generateModels"
:key="item.value"
class="model-item"
:class="{ active: model === item.value }"
@click="selectModel(item.value)"
>
{{ item.label }}
</div>
</div>
<div class="model-group">
<div class="group-title">编辑模型</div>
<div
v-for="item in editModels"
:key="item.value"
class="model-item"
:class="{ active: model === item.value }"
@click="selectModel(item.value)"
>
{{ item.label }}
</div>
</div>
<div class="model-group">
<div class="group-title">视觉理解模型</div>
<div
v-for="item in visionModels"
:key="item.value"
class="model-item"
:class="{ active: model === item.value }"
@click="selectModel(item.value)"
>
{{ item.label }}
</div>
</div>
</div>
<template #reference>
<div class="choice-btn">
<img src="@/assets/dialog/originalImage.svg" alt="" style="width: 16px;">
<span>{{ getModelLabel(model) }}</span>
</div>
</template>
</el-popover>
</template>
<script setup>
const props = defineProps({
modelValue: {
type: String,
default: 'flux'
}
})
const emit = defineEmits(['update:modelValue'])
const model = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const generateModels = [
{ value: 'flux', label: 'flux' },
{ value: 'Z-image', label: 'Z-image' },
{ value: 'jimeng', label: 'jimeng' },
{ value: 'Qwen-image', label: 'Qwen-image' }
]
const editModels = [
{ value: 'Banana-Pro', label: 'Banana-Pro' },
{ value: 'Qwen-image', label: 'Qwen-image' },
{ value: 'Banana', label: 'Banana' },
{ value: 'Kontext', label: 'Kontext' },
{ value: 'Jimeng_4.0', label: 'Jimeng.4.0' }
]
const visionModels = [
{ value: 'Qwen3.5plus', label: 'Qwen3.5plus' }
]
const selectModel = (value) => {
model.value = value
}
const getModelLabel = (value) => {
const allModels = [...generateModels, ...editModels, ...visionModels]
const model = allModels.find(item => item.value === value)
return model ? model.label : value
}
</script>
<style lang="less" scoped>
.choice-btn{
display: flex;
height: 40px;
padding: 0 15px;
justify-content: center;
align-items: center;
gap: 5px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.10);
background: #ffffff;
cursor: pointer;
position: relative;
}
.choice-btn:hover{
background: #E5E7EB;
}
.select{
padding: 10px;
max-height: 510px;
overflow-y: auto;
}
.model-group{
margin-bottom: 15px;
&:last-child{
margin-bottom: 0;
}
}
.group-title{
font-size: 12px;
font-weight: 600;
color: #666;
margin-bottom: 8px;
padding-left: 5px;
}
.model-item{
padding: 8px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-bottom: 4px;
transition: all 0.2s ease;
&:last-child{
margin-bottom: 0;
}
&:hover{
background: #f0f0f0;
}
&.active{
background: #e6f7ff;
color: #1890ff;
font-weight: 500;
}
}
</style>

View File

@ -0,0 +1,362 @@
<template>
<el-popover trigger="click" placement="top" :width="400">
<div class="proportion-container">
<div class="section">
<h3>选择比例</h3>
<div class="proportion-options">
<div
v-for="item in proportionOptions"
:key="item.value"
class="proportion-item"
:class="{ active: proportion === item.value }"
:style="getProportionStyle(item.value)"
@click="selectProportion(item.value)"
>
{{ item.label }}
</div>
</div>
</div>
<div class="section">
<h3>选择分辨率</h3>
<div class="resolution-options">
<div
v-for="item in resolutionOptions"
:key="item.value"
class="resolution-item"
:class="{ active: resolution === item.value }"
@click="selectResolution(item.value)"
>
{{ item.label }}
</div>
</div>
</div>
<div class="section">
<h3>尺寸(px)</h3>
<div class="size-inputs">
<div class="input-group">
<label>W</label>
<input type="number" v-model.number="width" @input="updateWidth">
</div>
<div class="lock-icon">🔒</div>
<div class="input-group">
<label>H</label>
<input type="number" v-model.number="height" @input="updateHeight">
</div>
</div>
</div>
</div>
<template #reference>
<div class="choice-btn">
<img src="@/assets/dialog/originalImage.svg" alt="" style="width: 16px;">
<span>{{ proportion }}</span>
</div>
</template>
</el-popover>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: '1:1'
}
})
const emit = defineEmits(['update:modelValue', 'update:width', 'update:height'])
const proportion = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const proportionOptions = [
{ value: '智能', label: '智能' },
{ value: '21:9', label: '21:9' },
{ value: '16:9', label: '16:9' },
{ value: '3:2', label: '3:2' },
{ value: '4:3', label: '4:3' },
{ value: '1:1', label: '1:1' },
{ value: '3:4', label: '3:4' },
{ value: '2:3', label: '2:3' },
{ value: '9:16', label: '9:16' }
]
const resolutionOptions = [
{ value: '1k', label: '标清 1K' },
{ value: '2k', label: '高清 2K' },
{ value: '4k', label: '超清 4K' }
]
const resolution = ref('2k')
const width = ref(2048)
const height = ref(2048)
const selectProportion = (value) => {
proportion.value = value
updateDimensionsByProportion(value)
}
const selectResolution = (value) => {
resolution.value = value
updateDimensionsByResolution(value)
}
const updateDimensionsByProportion = (proportionValue) => {
if (proportionValue === '智能') {
return
}
const [w, h] = proportionValue.split(':').map(Number)
const aspectRatio = w / h
if (width.value > height.value) {
height.value = Math.round(width.value / aspectRatio)
} else {
width.value = Math.round(height.value * aspectRatio)
}
emitUpdateDimensions()
}
const updateDimensionsByResolution = (resolutionValue) => {
let baseSize
switch (resolutionValue) {
case '1k':
baseSize = 1024
break
case '2k':
baseSize = 2048
break
case '4k':
baseSize = 4096
break
default:
baseSize = 2048
}
if (proportion.value === '智能') {
width.value = baseSize
height.value = baseSize
} else {
const [w, h] = proportion.value.split(':').map(Number)
const aspectRatio = w / h
if (aspectRatio > 1) {
width.value = baseSize
height.value = Math.round(baseSize / aspectRatio)
} else {
height.value = baseSize
width.value = Math.round(baseSize * aspectRatio)
}
}
emitUpdateDimensions()
}
const updateWidth = () => {
if (proportion.value !== '智能') {
const [w, h] = proportion.value.split(':').map(Number)
const aspectRatio = w / h
height.value = Math.round(width.value / aspectRatio)
}
emitUpdateDimensions()
}
const updateHeight = () => {
if (proportion.value !== '智能') {
const [w, h] = proportion.value.split(':').map(Number)
const aspectRatio = w / h
width.value = Math.round(height.value * aspectRatio)
}
emitUpdateDimensions()
}
const emitUpdateDimensions = () => {
emit('update:width', width.value)
emit('update:height', height.value)
}
const getProportionStyle = (value) => {
if (value === '智能') {
return {
'--width': '20px',
'--height': '20px'
}
}
const [w, h] = value.split(':').map(Number)
const aspectRatio = w / h
const baseSize = 20
if (aspectRatio > 1) {
return {
'--width': `${baseSize}px`,
'--height': `${Math.round(baseSize / aspectRatio)}px`
}
} else {
return {
'--width': `${Math.round(baseSize * aspectRatio)}px`,
'--height': `${baseSize}px`
}
}
}
//
updateDimensionsByResolution(resolution.value)
</script>
<style lang="less" scoped>
.choice-btn{
display: flex;
height: 40px;
padding: 0 15px;
justify-content: center;
align-items: center;
gap: 5px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.10);
background: #ffffff;
cursor: pointer;
position: relative;
}
.choice-btn:hover{
background: #E5E7EB;
}
.proportion-container{
padding: 10px;
}
.section{
margin-bottom: 20px;
&:last-child{
margin-bottom: 0;
}
h3{
font-size: 14px;
font-weight: 500;
margin-bottom: 12px;
color: #666;
}
}
.proportion-options{
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
margin-bottom: 16px;
background-color: #F8F9FA;
padding: 5px;
}
.proportion-item{
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
gap: 4px;
padding: 5px;
width: auto;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
border-radius: 5px;
text-align: bottom;
&::before{
content: '';
width: var(--width, 20px);
height: var(--height, 20px);
background: #f0f0f0;
border-radius: 4px;
transition: all 0.2s ease;
}
&:hover{
background: #e0e0e0;
}
&.active{
background: #ffffff;
}
}
.resolution-options{
display: flex;
padding: 5px;
align-items: center;
align-self: stretch;
border-radius: 10px;
background: #F8F9FA;
gap: 10px;
}
.resolution-item{
flex: 1;
padding: 10px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
text-align: center;
transition: all 0.2s ease;
background: #f5f5f5;
color: #666;
&:hover{
background: #e0e0e0;
}
&.active{
background: #ffffff;
color: #000000;
font-weight: 500;
}
}
.size-inputs{
display: flex;
align-items: center;
gap: 10px;
}
.input-group{
flex: 1;
position: relative;
label{
position: absolute;
top: 50%;
left: 12px;
transform: translateY(-50%);
font-size: 14px;
color: #666;
pointer-events: none;
}
input{
width: 100%;
padding: 12px 12px 12px 30px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
background: #f9f9f9;
&:focus{
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
}
.lock-icon{
font-size: 16px;
color: #999;
cursor: pointer;
&:hover{
color: #666;
}
}
</style>

View File

@ -0,0 +1,78 @@
<template>
<el-popover trigger="click" placement="top" :width="330">
<div class="quantity-container">
<div v-for="item in 4" :key="item" class="quantity-item" :class="{'selected': item == quantity}" @click="selectQuantity(item)">{{ item }}</div>
</div>
<template #reference>
<div class="choice-btn">
<img src="@/assets/dialog/originalImage.svg" alt="" style="width: 16px;">
<span>{{ quantity }} </span>
</div>
</template>
</el-popover>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: Number,
default: 1
}
})
const emit = defineEmits(['update:modelValue'])
const quantity = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const selectQuantity = (value) => {
quantity.value = value
}
</script>
<style lang="less" scoped>
.choice-btn{
display: flex;
height: 40px;
padding: 0 15px;
justify-content: center;
align-items: center;
gap: 5px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.10);
background: #ffffff;
cursor: pointer;
position: relative;
}
.choice-btn:hover{
background: #E5E7EB;
}
.quantity-container{
display: flex;
padding: 5px;
align-items: center;
align-self: stretch;
border-radius: 10px;
background: #F8F9FA;
}
.quantity-item{
display: flex;
width: 80px;
height: 32px;
padding: 0 10px;
justify-content: center;
align-items: center;
border-radius: 5px;
cursor: pointer;
}
.quantity-item.selected{
background: #fff;
}
.quantity-item:hover{
background: #E5E7EB;
}
</style>

15
src/main.js Normal file
View File

@ -0,0 +1,15 @@
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import VueVirtualScroller from 'vue-virtual-scroller'
import App from './App.vue'
import router from './router'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import './style.css'
const pinia = createPinia()
const app = createApp(App)
app.use(VueVirtualScroller)
app.use(pinia)
app.use(router)
app.mount('#app')

73
src/router/index.js Normal file
View File

@ -0,0 +1,73 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useDisplayStore, useUserStore } from '@/stores'
import { getToken } from '@/utils/auth'
const routes = [
{
path: '/',
redirect: '/home'
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue')
},
{
path: '/home',
name: 'home',
component: () => import('@/views/home/index.vue')
},
{
path: '/generate',
name: 'generate',
component: () => import('@/views/home/index.vue')
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// router.beforeEach(async (to, from, next) => {
// // 白名单路径(不需要验证 token 的路径)
// const whiteList = ['/login']
// // 如果访问的是白名单路径,直接放行
// if (whiteList.includes(to.path)) {
// return next()
// }
// // 检查是否有 token
// const token = getToken()
// if (!token) {
// // 没有 token重定向到登录页
// return next('/login')
// }
// useDisplayStore().sidebarShow = to.path.split('/')[1]
// // 获取用户 store 实例
// const userStore = useUserStore()
// // 检查 token 是否有效
// try {
// const isTokenValid = await userStore.checkTokenValid()
// console.log(isTokenValid)
// if (isTokenValid) {
// // token 有效,允许访问
// if (!userStore.userInfo.id) {
// // 如果用户信息不存在,则从服务器获取
// await userStore.getInfo()
// }
// next()
// } else {
// // token 无效,重定向到登录页
// next('/login')
// }
// } catch (error) {
// // 验证过程中出错,重定向到登录页
// console.error('验证 token 时出错:', error)
// next('/login')
// }
// })
export default router

9
src/stores/display.js Normal file
View File

@ -0,0 +1,9 @@
const DisplayStoreSetup = () => {
const Sender_variant = ref('default')
return {
Sender_variant
}
}
// eslint-disable-next-line no-undef
export const useDisplayStore = defineStore('display', DisplayStoreSetup)

3
src/stores/index.js Normal file
View File

@ -0,0 +1,3 @@
export * from './display'
export * from './param'
export * from './user'

8
src/stores/param.js Normal file
View File

@ -0,0 +1,8 @@
const ParamStoreSetup = () => {
return {
}
}
// eslint-disable-next-line no-undef
export const useParamStore = defineStore('params', ParamStoreSetup)

142
src/stores/user.js Normal file
View File

@ -0,0 +1,142 @@
import {
accountLogin as accountLoginApi,
checkUsertoken as checkUsertokenApi,
getCodePhone,
getUserInfo as getUserInfoApi,
logout as logoutApi,
phoneLogin as phoneLoginApi,
socialLogin as socialLoginApi
} from '@/apis/auth'
import { clearToken, getToken, setToken } from '@/utils/auth'
const storeSetup = () => {
const userInfo = reactive({
id: '',
username: '',
nickname: '',
gender: 0,
email: '',
phone: '',
avatar: '',
pwdResetTime: '',
pwdExpired: false,
registrationDate: '',
deptName: '',
roles: [],
permissions: [],
routers: []
})
const name = computed(() => userInfo.nickname)
const username = computed(() => userInfo.username)
const token = ref(getToken() || '')
const pwdExpiredShow = ref(true)
const roles = ref([]) // 当前用户角色
const permissions = ref([]) // 当前角色权限标识集合
const dept = ref({}) // 当前用户所在部门集合
const schoolURL = ref({ school: '', college: '' })
const isLogin = ref(false)
// 重置token
const resetToken = () => {
token.value = ''
clearToken()
}
// 检查token有效性
const checkTokenValid = async () => {
const res = await checkUsertokenApi()
console.log('checkTokenValid:', res) // 打印响应数据以进行调试
if (res.code === '401' || res.success === false) {
// 检查响应数据是否存在,以避免空响应导致的错误
console.error('Token is invalid:', res.message)// 打印错误信息以进行调试
return false
}
console.log('Token is valid') // 打印成功信息以进行调试
return true
}
// 获取用户信息
const getInfo = async () => {
const res = await getUserInfoApi()
Object.assign(userInfo, res.data)
userInfo.avatar = getAvatar(res.data.avatar, res.data.gender)
userInfo.username = res.data.username
if (typeof res.data.routers === 'string' && res.data.routers.trim() !== '') {
userInfo.routers = res.data.routers.split(',').map((item) => item.trim()) // 补充trim处理更完善
} else {
userInfo.routers = []
}
if (res.data.roles && res.data.roles.length) {
roles.value = res.data.roles
permissions.value = res.data.permissions
}
}
// 登录
const accountLogin = async (req) => {
const res = await accountLoginApi(req)
if (res.data == null || res.code === '500' || res.success === false) {
// eslint-disable-next-line no-undef
ElMessage({
title: '提示',
message: res.msg || '操作失败,请稍后重试'
})
isLogin.value = false
return false
}
setToken(res.data.token) // res.data.generateToken
token.value = res.data.token
getInfo()
// isLogin.value = true
return true
}
// 退出登录回调
const logoutCallBack = async () => {
roles.value = []
permissions.value = []
pwdExpiredShow.value = true
isLogin.value = false
resetToken()
}
// 退出登录
const logout = async () => {
try {
await logoutApi()
await logoutCallBack()
return true
} catch (error) {
console.error('Logout failed:', error.message) // 处理错误
return false
}
}
return {
userInfo,
name,
token,
roles,
permissions,
pwdExpiredShow,
dept,
username,
isLogin,
accountLogin,
logout,
logoutCallBack,
getInfo,
resetToken,
checkTokenValid
}
}
// eslint-disable-next-line no-undef
export const useUserStore = defineStore('user', storeSetup, {
persist: {
paths: ['token', 'roles', 'permissions', 'pwdExpiredShow', 'username'],
storage: localStorage
}
})

52
src/style.css Normal file
View File

@ -0,0 +1,52 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
/* width: 100%; */
font-family: "Microsoft YaHei";
font-size: 14px;
line-height: 1.5;
font-weight: 400;
color: #333333;
background-color: #f5f5f5;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
width: 100%;
height: 100vh;
min-width: 1500px;
/* min-height: 960px; */
overflow-y: hidden;
}
#app {
width: 100%;
height: 100%;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}

19
src/utils/auth.ts Normal file
View File

@ -0,0 +1,19 @@
const TOKEN_KEY = 'token'
const isLogin = () => {
return !!localStorage.getItem(TOKEN_KEY)
}
const getToken = () => {
return localStorage.getItem(TOKEN_KEY)
}
const setToken = (token: string) => {
localStorage.setItem(TOKEN_KEY, token)
}
const clearToken = () => {
localStorage.removeItem(TOKEN_KEY)
}
export { isLogin, getToken, setToken, clearToken }

78
src/utils/createTask.js Normal file
View File

@ -0,0 +1,78 @@
import workflows from '@/apis/workflows.json'
import { useParamStore } from '@/stores'
export async function getFormattedTime(date = new Date()) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0') // 月份从0开始需要+1
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
// 处理音频生成任务的数据并返回
export async function createTask(taskType = 1, params, title = '模特展示图') {
const paramStore = useParamStore()
const data = {
taskId: params.taskId,
taskRootId: params.taskRootId || paramStore.taskRootId,
parentTaskId: params.parentTaskId || '0',
AIGC: 'huanda',
platform: 'runninghub',
taskType,
modelName: 'Flux',
title,
file_type: params.file_type,
payload: {},
createTime: params.time,
parentCreateTime: params.parentCreateTime || '',
parentIndex: params.parentIndex || '',
token: params.token
}
if (taskType === 1) {
data.payload = workflows.huanda
data.payload.nodeInfoList[0].fieldValue = paramStore.params.clothes
data.payload.nodeInfoList[1].fieldValue = paramStore.params.model
data.payload.nodeInfoList[2].fieldValue = paramStore.params.pose
data.payload.nodeInfoList[3].fieldValue = paramStore.params.background
data.payload.nodeInfoList[4].fieldValue = paramStore.params.model ? 0 : 1
data.payload.nodeInfoList[5].fieldValue = paramStore.params.pose ? 0 : 1
data.payload.nodeInfoList[6].fieldValue = paramStore.params.background ? 0 : 1
data.payload.nodeInfoList[7].fieldValue = params.prompt
data.payload.nodeInfoList[7].fieldValue = params.aspectRatio
} else if (taskType === 2) { // 对话修改
data.parentTaskId = params.parentTaskId
data.payload = workflows.talk
data.payload.nodeInfoList[0].fieldValue = params.text
data.payload.nodeInfoList[1].fieldValue = params.talkImg
} else if (taskType === 3) { // 生成视频
data.parentTaskId = params.parentTaskId
data.payload = workflows.video
data.payload.nodeInfoList[0].fieldValue = params.text
data.payload.nodeInfoList[1].fieldValue = params.videoImg
} else if (taskType === 4) { // AI生成模特
data.payload = workflows.model
data.payload.nodeInfoList[0].fieldValue = params.text
data.payload.nodeInfoList[1].fieldValue = params.aspectRatio
} else if (taskType === 5 || taskType === 6) { // AI生成服装背景
// data.parentTaskId = params.parentTaskId
data.payload = workflows.background_pose
data.payload.nodeInfoList[0].fieldValue = params.text
data.payload.nodeInfoList[1].fieldValue = params.aspectRatio
}
console.log('data:', data)
return data
}
// 获取音频结果
export async function getTask(result) {
if (result.code === 0 && result.msg === 'success') {
return { type: true, url: result.data[0].fileUrl }
}
return { type: false, message: result.data.exception_message }
}

View File

@ -0,0 +1,76 @@
// 生成文件名的辅助函数
export async function generateFilename(url, prefix = 'image') {
try {
const urlObj = new URL(url)
const pathname = urlObj.pathname
let filename = pathname.substring(pathname.lastIndexOf('/') + 1)
// 如果URL中没有文件名或扩展名根据类型生成
if (!filename || !filename.includes('.')) {
const timestamp = new Date().getTime()
// 根据URL内容推断文件类型否则默认为png
const extension = url.includes('.jpg') || url.includes('.jpeg')
? '.jpg'
: url.includes('.gif')
? '.gif'
: url.includes('.webp') ? '.webp' : '.png'
filename = `${prefix}_${timestamp}${extension}`
}
return filename
} catch (error) {
console.error('URL解析失败:', error)
// 如果URL解析失败生成默认文件名
const timestamp = new Date().getTime()
return `${prefix}_${timestamp}.png`
}
}
// 通用下载图片函数
export async function downloadImage(imageUrl, filenamePrefix = 'image') {
if (!imageUrl) {
// 这里不能使用ElMessage因为在工具函数中
console.warn('暂无图片可下载')
return
}
try {
// 检查是否为外部链接
if (imageUrl.startsWith('http')) {
const response = await fetch(imageUrl)
const blob = await response.blob()
// 创建临时链接
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
// 生成合适的文件名
const filename = await generateFilename(imageUrl, filenamePrefix)
link.href = url
link.download = filename
// 添加到DOM并触发点击
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 记得释放对象URL
URL.revokeObjectURL(url)
console.log('图片下载成功')
} else {
// 如果是本地路径,直接下载
const link = document.createElement('a')
link.href = imageUrl
const filename = await generateFilename(imageUrl, filenamePrefix)
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
console.log('图片下载成功')
}
} catch (error) {
console.error('下载失败:', error)
}
}

39
src/utils/encrypt.ts Normal file
View File

@ -0,0 +1,39 @@
import Base64 from 'crypto-js/enc-base64'
import UTF8 from 'crypto-js/enc-utf8'
import { JSEncrypt } from 'jsencrypt'
import md5 from 'crypto-js/md5'
import CryptoJS from 'crypto-js'
export function encodeByBase64(txt: string) {
return UTF8.parse(txt).toString(Base64)
}
export function decodeByBase64(txt: string) {
return Base64.parse(txt).toString(UTF8)
}
export function encryptByMd5(txt: string) {
return md5(txt).toString()
}
const publicKey
= 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAM51dgYtMyF+tTQt80sfFOpSV27a7t9u'
+ 'aUVeFrdGiVxscuizE7H8SMntYqfn9lp8a5GH5P1/GGehVjUD2gF/4kcCAwEAAQ=='
export function encryptByRsa(txt: string) {
const encryptor = new JSEncrypt()
encryptor.setPublicKey(publicKey) // 设置公钥
return encryptor.encrypt(txt) // 对数据进行加密
}
const defaultKeyWork = 'XwKsGlMcdPMEhR1B'
export function encryptByAes(word: string, keyWord = defaultKeyWork) {
const key = CryptoJS.enc.Utf8.parse(keyWord)
const arcs = CryptoJS.enc.Utf8.parse(word)
const encrypted = CryptoJS.AES.encrypt(arcs, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
return encrypted.toString()
}

86
src/utils/request.js Normal file
View File

@ -0,0 +1,86 @@
import axios from 'axios'
import { getToken } from '@/utils/auth'
import { userError } from './tokenError'
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 50000 // 请求超时时间
})
const StatusCodeMessage = {
200: '服务器成功返回请求的数据',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)',
204: '删除数据成功',
400: '请求错误(400)',
401: '未授权,请重新登录(401)',
403: '拒绝访问(403)',
404: '请求出错(404)',
408: '请求超时(408)',
500: '服务器错误(500)',
501: '服务未实现(501)',
502: '网络错误(502)',
503: '服务不可用(503)',
504: '网络超时(504)'
}
// request拦截器
service.interceptors.request.use(
(config) => {
const token = getToken()
if (token) {
if (!config.headers) {
config.headers = {}
}
config.headers.Authorization = `Bearer ${token}`
}
// console.log(config.baseURL)
if (config.url?.startsWith(import.meta.env.VITE_API_PAY_PREFIX)) { // 支付服务路由
config.baseURL = import.meta.env.VITE_API_PAY_TARGET
} else if (config.url?.startsWith(import.meta.env.VITE_API_AIGC_PREFIX)) { // 资源服务路由
// config.url = config.url.replace(import.meta.env.VITE_API_AIGC_PREFIX, '')
config.baseURL = import.meta.env.VITE_API_AIGC_TARGET
} else if (config.url?.startsWith(import.meta.env.VITE_API_MUSIC_WORKFLOW_PREFIX)) { // 音频生成平台工作流服务路由
config.url = config.url.replace(import.meta.env.VITE_API_MUSIC_WORKFLOW_PREFIX, '')
config.baseURL = import.meta.env.VITE_API_MUSIC_WORKFLOW_TARGET
}
return config
},
(error) => {
// Do something with request error
Promise.reject(error)
}
)
// response 拦截器
service.interceptors.response.use(
(response) => {
const { data } = response
const { success, code, msg } = data
if (success || code === 0) {
console.log('msg: \n', msg)
return response.data
} else if (code === 401 && response.config.url !== '/auth/check/token`') { // 判断code=401时进行页面刷新但是不对检验token这个路由的请求判断防止出现死循环
userError()
}
console.log('CodeMessage: \n', StatusCodeMessage[code])
console.log('msg: \n', msg)
return response.data
},
(error) => {
console.log('err: \n', error)
return Promise.reject(error)
}
)
// 添加HTTP DELETE方法
service.del = function (url, config) {
return service({
method: 'delete',
url,
...config
})
}
export default service

15
src/utils/tokenError.js Normal file
View File

@ -0,0 +1,15 @@
export function userError() {
// eslint-disable-next-line no-undef
ElNotification({
title: '身份错误',
// eslint-disable-next-line no-undef
message: h('i', { style: 'color: teal' }, '检测到身份验证失败,请手动刷新页面,\n或者5秒后自动刷新页面重试'),
type: 'error',
duration: 5000
})
Promise.resolve().then(() => {
setTimeout(() => {
window.location.reload()
}, 5000)
})
}

0
src/utils/uploadImage.js Normal file
View File

402
src/utils/websocket.js Normal file
View File

@ -0,0 +1,402 @@
import { ElNotification } from 'element-plus'
import { h, ref } from 'vue'
import { setTaskRootId } from '@/apis/createRoot'
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
import { getToken } from '@/utils/auth'
import { createTask, getFormattedTime, getTask } from '@/utils/createTask'
import { userError } from '@/utils/tokenError'
export function websocketError(code, msg) {
let message
switch (code) {
case 1006:
message = '用户身份验证失败'
userError()
break
case 4401: // 后端返回常规错误
message = msg
break
case 4402: // 后端返回外部平台提交时的错误
message = JSON.parse(msg)
break
case 4403: // 外部平台的任务结果的错误
message = msg
break
default:
message = '连接异常,请稍后重试'
}
ElNotification({
title: '生成通知',
message: h('i', { style: 'color: teal' }, message),
type: 'error'
})
}
export function websocketSuccess() {
// 合并两个通知为一个
ElNotification({
title: '生成通知',
message: h('div', [
h('div', { style: 'font-weight: bold; color: teal;' }, '生成成功!'),
h('br'),
h('div', { style: 'color: orange; margin-top: 5px;' }, '内测状态请及时下载生成的文件云端储存与历史记录保留24小时')
]),
type: 'success',
duration: 6000 // 增加持续时间以适应更多信息
})
}
export async function generateRootImage(aspectRatio, prompt) {
const progress_text = ref('')
const message = ref('')
const useDisplay = useDisplayStore()
const useParams = useParamStore()
const useUser = useUserStore()
const newTaskRootId = crypto.randomUUID()
const taskId = crypto.randomUUID()
const time = await getFormattedTime()
const newRootTaskImage = useParams.rootTaskImage
const token = getToken()
const result = await createTask(1, { prompt, aspectRatio, time, taskRootId: newTaskRootId, taskId, token })
// const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjY0NDEwODAyMjk1OTgzNzIzMCwicm5TdHIiOiJiWkVwS2JLWFJyZmRIaFFHWXZKTkdzOGdGM0JSRmxQOCJ9.5eQ2GtVdrDntQDe2tnF8vl_DhTfd2uW-KNqzvl1imc0'
const wsURL = `${import.meta.env.VITE_API_WORKFLOW_WS}/?token=${token}`
const socket = new WebSocket(wsURL)
console.log('WebSocket连接已建立')
// 心跳机制相关变量
let heartbeatInterval = null
const heartbeatIntervalTime = 20000 // 30秒发送一次心跳
try {
// 接收服务器消息
socket.onmessage = async (event) => {
// 处理pong响应
if (event.data === 'pong') {
console.log('收到心跳响应')
return
} else if (event.data === 'please give me taskId') {
socket.send(`setTaskId:${taskId}`)
progress_text.value = '信息提交中...'
return
} else if (event.data === 'OK! Please continue. ') {
// 发送消息
useParams.newTaskRootId = newTaskRootId
useParams.taskRootId = newTaskRootId
useDisplay.taskRootId = newTaskRootId
socket.send(JSON.stringify({
type: 'generate',
data: result
}))
return
} else if (event.data === '任务提交成功,正在排队中...') {
progress_text.value = '生成中...'
useParams.setRootTask = { id: taskId, type: 'image', status: 'generate', name: '模特展示图', time, files: [] }
return
}
message.value = event.data
}
// 处理链接错误
socket.onerror = (error) => {
console.error('WebSocket链接出错:', error)
// 清理心跳定时器
if (heartbeatInterval) {
console.log('清理心跳定时器')
clearInterval(heartbeatInterval)
}
}
// 处理链接关闭
socket.onclose = async (event) => {
console.log('WebSocket已关闭:', event)
useDisplay.isRootGerenate = false
// 清理心跳定时器
if (heartbeatInterval) {
console.log('清理心跳定时器')
clearInterval(heartbeatInterval)
}
if (event.code === 1006) {
console.error('用户身份验证失败')
userError()
return
}
if (event.code === 1000 && event.reason === 'success') {
const res = JSON.parse(message.value)
console.log('收到服务器消息:', res)
const result = await getTask(res)
if (result.type) {
useParams.setRootTask = { id: taskId, type: 'image', status: 'success', name: '模特展示图', time, files: [result.url] }
setTaskRootId({ taskRootId: newTaskRootId, userId: useUser.userInfo.id, imageUrl: newRootTaskImage })
useDisplay.getNewHistory = true
websocketSuccess()
} else {
websocketError(4403, res.message)
}
} else {
websocketError(event.code, event.reason)
useParams.setRootTask = { id: taskId, type: 'image', status: 'error', name: '生成失败', time, files: [] }
}
}
// 等待 WebSocket 连接打开
socket.onopen = async () => {
console.log('WebSocket连接已建立')
// 启动心跳机制
heartbeatInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send('ping')
console.log('发送心跳包')
}
}, heartbeatIntervalTime)
}
} catch (error) {
console.log('Error creating AI3D_file:', error)
ElNotification({
title: '生成通知',
message: h('i', { style: 'color: teal' }, '生成失败,请检查参数后重新提交任务'),
type: 'error'
})
}
}
export async function generateSubImage(taskType, data, title) {
const progress_text = ref('')
const message = ref('')
const useParams = useParamStore()
const useDisplay = useDisplayStore()
const newTaskRootId = useDisplay.taskRootId
console.log('newTaskRootId', newTaskRootId)
useParams.taskRootId = newTaskRootId
const taskId = crypto.randomUUID()
const time = await getFormattedTime()
const token = getToken()
const result = await createTask(taskType, { ...data, time, taskId, taskRootId: newTaskRootId, token }, title)
// const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjY0NDEwODAyMjk1OTgzNzIzMCwicm5TdHIiOiJiWkVwS2JLWFJyZmRIaFFHWXZKTkdzOGdGM0JSRmxQOCJ9.5eQ2GtVdrDntQDe2tnF8vl_DhTfd2uW-KNqzvl1imc0'
const wsURL = `${import.meta.env.VITE_API_WORKFLOW_WS}/?token=${token}`
const socket = new WebSocket(wsURL)
console.log('WebSocket连接已建立')
// 心跳机制相关变量
let heartbeatInterval = null
const heartbeatIntervalTime = 20000 // 30秒发送一次心跳
try {
// 接收服务器消息
socket.onmessage = async (event) => {
// 处理pong响应
if (event.data === 'pong') {
console.log('收到心跳响应')
return
} else if (event.data === 'please give me taskId') {
socket.send(`setTaskId:${taskId}`)
progress_text.value = '信息提交中...'
return
} else if (event.data === 'OK! Please continue. ') {
// 发送消息
socket.send(JSON.stringify({
type: 'generate',
data: result
}))
return
} else if (event.data === '任务提交成功,正在排队中...') {
progress_text.value = '生成中...'
useParams.setSubTask = { id: taskId, type: data.file_type, status: 'generate', name: title, time, parentIndex: data.parentIndex, parentTime: data.parentCreateTime, files: [] }
useDisplay.AIvideo = false
return
}
message.value = event.data
}
// 处理链接错误
socket.onerror = (error) => {
console.error('WebSocket链接出错:', error)
// 清理心跳定时器
if (heartbeatInterval) {
clearInterval(heartbeatInterval)
}
ElNotification({
title: '生成通知',
message: h('i', { style: 'color: teal' }, '生成视频失败'),
type: 'error'
})
}
// 处理链接关闭
socket.onclose = async (event) => {
console.log('WebSocket已关闭:', event)
useDisplay.isSubGerenate = false
// 清理心跳定时器
if (heartbeatInterval) {
clearInterval(heartbeatInterval)
}
if (event.code === 1006) {
console.error('用户身份验证失败')
userError()
} else if (event.code === 1000 && event.reason === 'success') {
const res = JSON.parse(message.value)
console.log('收到服务器消息:', res)
const result = await getTask(res)
if (result.type) {
useParams.setSubTask = { id: taskId, type: data.file_type, status: 'success', name: title, time, parentIndex: data.parentIndex, parentTime: data.parentCreateTime, files: [result.url] }
websocketSuccess()
} else {
websocketError(4403, res.message)
}
} else {
websocketError(event.code, event.reason)
useParams.setSubTask = { id: taskId, type: 'image', status: 'error', name: '生成失败', time, files: [] }
}
// 清理心跳定时器
if (heartbeatInterval) {
clearInterval(heartbeatInterval)
}
}
// 等待 WebSocket 连接打开
socket.onopen = async () => {
console.log('WebSocket连接已建立')
// 启动心跳机制
heartbeatInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send('ping')
console.log('发送心跳包')
}
}, heartbeatIntervalTime)
}
} catch (error) {
console.log('Error creating AI3D_file:', error)
ElNotification({
title: '生成通知',
message: h('i', { style: 'color: teal' }, '生成失败,请检查参数后重新提交任务'),
type: 'error'
})
}
}
export async function generateAIimage(taskType, data, type) {
const progress_text = ref('')
const message = ref('')
const useDisplay = useDisplayStore()
const token = getToken()
const taskId = crypto.randomUUID()
const result = await createTask(taskType, { text: data.prompt, aspect_ratio: data.aspectRatio, token })
// const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOjY0NDEwODAyMjk1OTgzNzIzMCwicm5TdHIiOiJiWkVwS2JLWFJyZmRIaFFHWXZKTkdzOGdGM0JSRmxQOCJ9.5eQ2GtVdrDntQDe2tnF8vl_DhTfd2uW-KNqzvl1imc0'
const wsURL = `${import.meta.env.VITE_API_WORKFLOW_WS}/?token=${token}`
const socket = new WebSocket(wsURL)
console.log('WebSocket连接已建立')
// 心跳机制相关变量
let heartbeatInterval = null
const heartbeatIntervalTime = 20000 // 30秒发送一次心跳
try {
// 接收服务器消息
socket.onmessage = async (event) => {
// 处理pong响应
if (event.data === 'pong') {
console.log('收到心跳响应')
return
} else if (event.data === 'please give me taskId') {
socket.send(`setTaskId:${taskId}`)
progress_text.value = '信息提交中...'
return
} else if (event.data === 'OK! Please continue. ') {
// 发送消息
socket.send(JSON.stringify({
type: 'generate',
data: result
}))
return
} else if (event.data === '任务提交成功,正在排队中...') {
progress_text.value = '视频生成中...'
return
}
message.value = event.data
}
// 处理链接错误
socket.onerror = (error) => {
console.error('WebSocket链接出错:', error)
// 清理心跳定时器
if (heartbeatInterval) {
clearInterval(heartbeatInterval)
}
}
// 处理链接关闭
socket.onclose = async (event) => {
console.log('WebSocket已关闭:', event)
useDisplay.isAIgenerate[type] = false
// 清理心跳定时器
if (heartbeatInterval) {
console.log('清理心跳定时器')
clearInterval(heartbeatInterval)
}
if (event.code === 1006) {
console.error('用户身份验证失败')
userError()
return
}
if (event.code === 1000 && event.reason === 'success') {
const res = JSON.parse(message.value)
console.log('收到服务器消息:', res)
const result = await getTask(res)
if (result.type) {
useDisplay.AIimage[type] = result.url
console.log('生成成功', useDisplay.AIimage[type])
websocketSuccess()
} else {
websocketError(4403, res.message)
}
} else {
websocketError(event.code, event.reason)
}
}
// 等待 WebSocket 连接打开
socket.onopen = () => {
console.log('WebSocket连接已建立')
// 启动心跳机制
heartbeatInterval = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send('ping')
console.log('发送心跳包')
}
}, heartbeatIntervalTime)
// 发送消息
socket.send(JSON.stringify({
type: 'generate',
data: result
}))
}
} catch (error) {
console.log('Error creating AI3D_file:', error)
ElNotification({
title: '生成通知',
message: h('i', { style: 'color: teal' }, '生成失败,请检查参数后重新提交任务'),
type: 'error'
})
}
}

View File

@ -0,0 +1,363 @@
<template>
<div style="width: 100%;display: flex;justify-content: center;align-items: center;">
<div class="primary-box" :class="{ 'none-primary-box': props.item.status === 'none' }">
<!-- 标题 -->
<div class="title">
<div class="style">
<span class="name">{{ props.item.name }}</span>
<span v-show="props.item.time" class="time">({{ props.item.time }})</span>
</div>
<div v-if="props.item.parentIndex" class="dividing-line"></div>
<div v-if="props.item.parentIndex" class="style">
<span class="time">源自 {{ props.item.parentTime }}</span>
<span class="time"> {{ props.item.parentIndex }} 张图片</span>
</div>
</div>
<!-- 加载中 -->
<div v-if="props.item.status === 'none'" class="box none-box">
<!-- <img :src="primaryPicture" alt="无" class="img" /> -->
</div>
<!-- 生成失败 -->
<div v-if="props.item.status === 'error'" class="box none-box">
<img :src="primaryPicture" alt="无" class="img" />
<!-- <span>生成失败</span> -->
</div>
<!-- 生成中 -->
<div v-if="props.item.status === 'generate'" class="box generate-box">
<div class="generate-content">
<!-- 欢快的加载动画 -->
<div class="loading-animation">
<div class="bounce-dot"></div>
<div class="bounce-dot"></div>
<div class="bounce-dot"></div>
</div>
<!-- 状态文本 -->
<div class="status-text">{{ generateStatusText }}</div>
</div>
</div>
<!-- 已完成 -->
<div v-if="props.item.status === 'success'" class="box success-box">
<div v-for="(file, index) in props.item.files" :key="index" class="one-box">
<img :src="file" alt="index" class="img" />
<div class="left-top">
<div class="left-top-btn" @click="downloadImage(file, 'image')"><img src="@/assets/display/download.svg" /></div>
<span class="line" />
<div class="left-top-btn" @click="addCollection(file)"><img src="@/assets/display/collection.svg" /></div>
</div>
<div class="bottom-brush">
<el-tooltip
effect="dark"
content="画笔"
placement="top"
>
<div class="brush" @click.stop="AIvideo(file, index)"><img :src="brush" /></div>
</el-tooltip>
</div>
</div>
</div>
<div v-if="props.item.status === 'success'" class="bottom-btn-group">
<div v-for="(item, index) in bottomBtnGroup" :key="index" class="bottom-btn" @click="item.click(file, index)">
<img :src="item.icon" />
<span>{{ item.name }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import brush from '@/assets/display/brush.svg'
import { setCollectImg } from '@/apis/collection'
import { useDisplayStore, useParamStore, useUserStore } from '@/stores'
import { downloadImage } from '@/utils/downloadImage.js'
const props = defineProps({
item: {
type: Object,
default: () => ({})
}
})
const useDisplay = useDisplayStore()
const useParams = useParamStore()
const useUser = useUserStore()
const reEdit = (url, number) => {
console.log(number)
// const index = String(number + 1)
// useDisplay.DialogModification = true
// useDisplay.AIvideo = false
// useParams.dialogModificationImage = { title: '', url, time: props.item.time, parentIndex: index, parentTaskId: props.item.id }
}
const againGenerate = (url, number) => {
console.log(number)
// const index = String(number + 1)
// useDisplay.AIvideo = true
// useDisplay.DialogModification = false
// useParams.AIvideoImage = { title: '', url, time: props.item.time, parentIndex: index, parentTaskId: props.item.id }
}
const deleteImage = (url, number) => {
console.log(number)
// const index = String(number + 1)
// useDisplay.deleteImage = true
// useParams.deleteImage = { title: '', url, time: props.item.time, parentIndex: index, parentTaskId: props.item.id }
}
const bottomBtnGroup = [
{
name: '重新编辑',
icon: '@/assets/display/dialogModification.svg',
click: reEdit
},
{
name: '再次生成',
icon: '@/assets/display/AIvideo.svg',
click: againGenerate
},
{
name: '删除该批次',
icon: '@/assets/display/delete.svg',
click: deleteImage
}
]
const addCollection = (url) => {
setCollectImg({ userId: useUser.userInfo.id, url, clothesId: 0 }).then((res) => {
if (res.code === '0' && res.success === true) {
// eslint-disable-next-line no-undef
ElMessage.success('添加收藏成功')
} else {
// eslint-disable-next-line no-undef
ElMessage.error(res.msg || '添加收藏失败')
}
})
}
</script>
<style lang="less" scoped>
.primary-box{
width: 100%;
max-width: 1024px;
display: flex;
flex-direction: column;
flex: 1;
gap: 20px;
padding-bottom: 20px;
}
.none-primary-box{
height: 350px;
}
.title{
height: 25px;
width: 100%;
display: flex;
align-items: center;
gap: 20px;
.style{
display: flex;
align-items: center;
gap: 10px;
}
.name{
color: #333;
font-family: "Microsoft YaHei";
font-size: 15px;
font-style: normal;
font-weight: 700;
line-height: normal;
}
.time{
color: #333;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
}
.dividing-line{
height: 100%;
border: 1px solid #ccc;
}
.delete-btn{
cursor: pointer;
}
.delete-btn:hover{
color: #ff4949;
}
}
.box{
height: calc(100% - 37px);
width: 100%;
}
.none-box{
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.img{
width: 100%;
max-width: 300px;
}
}
//
.generate-box {
height: 300px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 20px;
.generate-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
.status-text {
font-size: 14px;
color: #333;
font-weight: 600;
font-family: "Microsoft YaHei";
}
}
}
//
.loading-animation {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
.bounce-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #333;
animation: bounce 1.5s infinite ease-in-out;
&:nth-child(2) {
animation-delay: 0.2s;
background-color: #409eff;
}
&:nth-child(3) {
animation-delay: 0.4s;
background-color: #67c23a;
}
}
}
@keyframes bounce {
0%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-15px);
}
}
//
.one-box{
position: relative;
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
border-radius: 10px;
// height: 100%;
}
.one-box:hover{
.left-top,.bottom-brush{
display:flex
}
}
.success-box{
display: grid;
grid-template-columns: repeat(4, 1fr);
border-radius: 10px;
gap: 10px;
.img{
width: 100%;
height: auto; /* 保持宽高比 */
object-fit: contain; /* 确保图片完整显示,不被裁剪 */
border-radius: 8px; /* 可选:给图片添加圆角 */
}
}
.left-top,.bottom-brush{
display: none;
position: absolute;
z-index: 1;
cursor: pointer;
justify-content: center;
align-items: center;
}
.left-top{
padding: 4px 6px;
gap: 10px;
right: 10px;
top: 10px;
border-radius: 5px;
background: rgba(51, 51, 51, 0.80);
backdrop-filter: blur(2.5px);
.left-top-btn{
display: flex;
align-items: center;
padding: 4px;
border-radius: 4px;
}
.line{
width: 1px;
height: 12px;
background-color: #ccc;
}
}
.bottom-brush{
width: 60px;
height: 32px;
border-radius: 10px;
background: rgba(51, 51, 51, 0.80);
backdrop-filter: blur(2.5px);
bottom: 10px;
}
.left-top-btn:hover,.bottom-brush:hover {
background-color: rgb(112, 112, 112);
}
//
.bottom-btn-group{
display: flex;
align-items: center;
gap: 10px;
}
.bottom-btn{
display: flex;
align-items: center;
gap: 5px;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
}
.bottom-btn:hover{
background-color: #e4e7ed;
}
</style>

View File

@ -0,0 +1,274 @@
<template>
<div id="display" class="content-area">
<div class="back">
<img src="@/assets/display/back.svg" alt="">
<span class="title-text">退出</span>
</div>
<div v-if="!props.if" class="btn-container">
<div class="btn" @click="activeTab = 'all'">
<!-- <span class="btn-text">全部</span> -->
<img src="@/assets/display/arrow.svg" alt="">
</div>
<span class="line"></span>
<div class="btn" @click="activeTab = 'success'">
<span class="btn-text">时间</span>
<img src="@/assets/display/arrow.svg" alt="">
</div>
<span class="line"></span>
<div class="btn" @click="activeTab = 'none'">
<span class="btn-text">收藏</span>
<img src="@/assets/display/arrow.svg" alt="">
</div>
</div>
<DynamicScroller
v-if="!props.if"
:items="list"
:min-item-size="54"
class="scroller"
:buffer="250"
@scroll="handleScroll"
@scroll-end="getList"
>
<template #default="{ item, index, active }">
<DynamicScrollerItem
:item="item"
:active="active"
:data-index="index"
>
<Set :key="`${item.id}`" :item="item" />
</DynamicScrollerItem>
</template>
</DynamicScroller>
</div>
</template>
<script setup>
import { getTaskListByRootId } from '@/apis/history'
import { useDisplayStore, useParamStore } from '@/stores'
import Set from './components/set.vue'
const props = defineProps({
if: {
type: Boolean,
default: false
}
})
const useDisplay = useDisplayStore()
const useParams = useParamStore() //
const activeTab = ref('all')
// const tempList = ref([])
const tempList = ref([
{ id: 0, type: 'image', status: 'none', name: '局部重绘', time: '2025-12-01 18:26', files: [] },
{ id: 1, type: 'image', status: 'success', name: '局部重绘', time: '2025-12-01 18:26', parentName: '2', parentTime: '2025-12-01 12:26', files: ['https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png'] },
{ id: 2, type: 'image', status: 'success', name: '局部重绘', time: '2025-12-01 18:26', parentName: '2', parentTime: '2025-12-01 12:26', files: ['https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png'] },
{ id: 3, type: 'image', status: 'success', name: '局部重绘', time: '2025-12-01 18:26', parentName: '2', parentTime: '2025-12-01 12:26', files: ['https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png'] },
{ id: 4, type: 'image', status: 'success', name: '局部重绘', time: '2025-12-01 18:26', parentName: '2', parentTime: '2025-12-01 12:26', files: ['https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png', 'https://sxwz.xueai.art/static/png/image4-1-1-BZWQeEAk.png'] }
])
const list = ref()
const page = ref(1)
list.value = tempList.value
//
const toggleDisplay = (newValue, oldValue) => {
if ((newValue === 'image' || newValue === 'video') && oldValue === 'all') {
list.value = tempList.value.filter((item) => item.type === newValue)
} else if ((newValue === 'image' || newValue === 'video') && (oldValue === 'image' || oldValue === 'video')) {
list.value = tempList.value.filter((item) => item.type === newValue)
} else {
list.value = tempList.value
}
console.log(list.value)
}
//
const conversion = (newlist) => {
const temp = newlist.data.records.map((item) => {
return {
id: item.taskId,
type: item.taskType === 1 || item.taskType === 2 ? 'image' : 'video',
status: 'success',
name: item.title,
time: item.createTime,
parentId: item.parentTaskId,
parentName: item.parentName || '',
parentTime: item.parentCreateTime || '',
parentIndex: item.parentIndex,
files: [item.fileUrl]
}
})
return temp
}
//
const getList = async () => {
// if (page.value === 0) return
// page.value++
// console.log('getList', page.value)
// const newlist = await getTaskListByRootId({ taskRootId: useParams.taskRootId, page: page.value, size: 15 })
// if (newlist.data.records.length === 0) {
// page.value = 0
// return
// }
// const temp = conversion(newlist)
// list.value.push(...temp)
// tempList.value.push(...temp)
}
//
const handleScroll = (event) => {
const { scrollTop, scrollHeight, clientHeight } = event.target
const distanceToBottom = scrollHeight - scrollTop - clientHeight
// console.log('distanceToBottom', distanceToBottom)
if (distanceToBottom <= 50) {
useDisplay.Sender_variant = 'updown'
} else if (distanceToBottom >= 350) {
useDisplay.Sender_variant = 'default'
}
}
// tab
watch(() => activeTab.value, (newValue, oldValue) => {
console.log('activeTab', newValue, oldValue)
toggleDisplay(newValue, oldValue)
})
//
watch(() => useParams.setRootTask, (newValue) => {
// console.log('setDisplayList', newValue)
console.log('setRootTask', newValue)
console.log(useParams.newTaskRootId, useParams.taskRootId)
if (useParams.newTaskRootId !== useParams.taskRootId) return
console.log('是否在生成', useDisplay.isRootGerenate)
if (useDisplay.isRootGerenate) {
tempList.value = [newValue]
console.log('初始化tempList', tempList.value)
if (activeTab.value !== 'all') {
activeTab.value = 'all'
return
}
list.value = [...tempList.value]
// Object.assign(list.value[0], newValue)
console.log('初始化list', list.value)
return
}
Object.assign(tempList.value[0], newValue)
if (activeTab.value === newValue.type || activeTab.value === 'all') Object.assign(list.value[0], newValue)
console.log('tempList更新', tempList.value)
console.log('list更新', list.value)
})
//
watch(() => useParams.setSubTask, (newValue) => {
// console.log('setDisplayList', newValue)
console.log('setSubTask', newValue)
console.log(useDisplay.taskRootId, useParams.taskRootId)
if (useDisplay.taskRootId !== useParams.taskRootId) return
if (useDisplay.isSubGerenate) {
tempList.value.unshift(newValue)
if (activeTab.value === newValue.type) list.value.unshift(newValue)
console.log('初始化子任务tempList', tempList.value)
console.log('初始化子任务list', list.value)
return
}
Object.assign(tempList.value[0], newValue)
if (activeTab.value === newValue.type) Object.assign(list.value[0], newValue)
console.log('tempList更新', tempList.value)
console.log('list更新', list.value)
})
// id
watch(() => useDisplay.taskRootId, async (newValue) => {
console.log('选择历史记录', newValue)
const newlist = await getTaskListByRootId({ taskRootId: newValue, page: 1, size: 15 })
const temp = conversion(newlist)
tempList.value = temp
list.value = tempList.value
page.value = 1
console.log('初始化list', list.value)
useParams.taskRootId = newValue
})
onMounted(() => {
console.log('display mounted')
useDisplay.displayOnMounted = true
})
</script>
<style lang="less" scoped>
.content-area {
width: 100%;
min-width: 750px;
height: 100%;
overflow-y: auto;
transition: all 0.3s ease;
}
.back{
display: flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 10px;
position: absolute;
left: 30px;
top: 22px;
z-index: 3;
cursor: pointer;
border-radius: 10px;
// background-color: #FAFBFC;
}
.back:hover{
background-color: #e4e7ed;
}
.btn-container{
display: flex;
align-items: center;
gap: 6px;
height: 36px;
padding: 4px;
right: 30px;
top: 22px;
z-index: 3;
border-radius: 10px;
background-color: #FAFBFC;
position: absolute;
.btn{
display: flex;
padding: 5px;
align-items: center;
gap: 5px;
}
.btn-text{
color: #000;
font-family: "Microsoft YaHei";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
text-align: center;
}
.line{
width: 1px;
height: 8px;
background-color: #ccc;
}
}
.scroller {
height: 100%;
padding: 30px 0px 170px 0px;
will-change: scroll-position;
-webkit-overflow-scrolling: touch; /* iOS Safari */
scroll-behavior: smooth; /* 平滑滚动 */
&::-webkit-scrollbar-track {
background: transparent; /* 轨道透明 */
}
}
</style>

28
src/views/home/index.vue Normal file
View File

@ -0,0 +1,28 @@
<script setup>
import { useRoute } from 'vue-router'
import display from './display/index.vue'
const route = useRoute()
const shouldShowDisplay = route.path === '/home'
</script>
<template>
<div class="app-container">
<dialogBox :is-generate="shouldShowDisplay" />
<display :if="shouldShowDisplay" />
</div>
</template>
<style lang="less" scoped>
/* app-container 设置 */
.app-container {
width: 100%;
height: 100%;
position: relative;
background-color: #ffffff;
overflow: hidden;
}
</style>

328
src/views/login/index.vue Normal file
View File

@ -0,0 +1,328 @@
<template>
<div class="login-container">
<div class="login-content">
<div class="icon-wrapper">
<div class="lock-icon">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C9.243 2 7 4.243 7 7V10H6C4.897 10 4 10.897 4 12V20C4 21.103 4.897 22 6 22H18C19.103 22 20 21.103 20 20V12C20 10.897 19.103 10 18 10H17V7C17 4.243 14.757 2 12 2ZM12 4C13.654 4 15 5.346 15 7V10H9V7C9 5.346 10.346 4 12 4ZM6 12H18V20H6V12Z" fill="currentColor"/>
</svg>
</div>
</div>
<h1 class="title">账号未登录</h1>
<p class="description">请先登录以访问完整功能</p>
<div class="action-buttons">
<button class="btn-primary" @click="handleLogin">
立即登录
</button>
<button class="btn-secondary" @click="handleRegister">
注册账号
</button>
</div>
<div class="features">
<div class="feature-item">
<div class="feature-icon"></div>
<span>AI智能绘画</span>
</div>
<div class="feature-item">
<div class="feature-icon">🎨</div>
<span>多种模型</span>
</div>
<div class="feature-item">
<div class="feature-icon"></div>
<span>快速生成</span>
</div>
</div>
</div>
<div class="background-decoration">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
<div class="circle circle-3"></div>
</div>
</div>
</template>
<script setup>
defineOptions({ name: 'LoginPage' })
const handleLogin = () => {
window.location.href = 'https://sxwz.xueai.art/login'
}
const handleRegister = () => {
console.log('跳转到注册页面')
}
</script>
<style scoped>
.login-container {
position: relative;
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
overflow: hidden;
}
.login-content {
position: relative;
z-index: 10;
text-align: center;
padding: 60px 40px;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: slideUp 0.8s ease-out;
max-width: 500px;
width: 90%;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.icon-wrapper {
margin-bottom: 30px;
}
.lock-icon {
width: 80px;
height: 80px;
margin: 0 auto;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: white;
animation: pulse 2s ease-in-out infinite;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
50% {
transform: scale(1.05);
box-shadow: 0 15px 40px rgba(102, 126, 234, 0.6);
}
}
.lock-icon svg {
width: 40px;
height: 40px;
animation: shake 3s ease-in-out infinite;
}
@keyframes shake {
0%, 100% {
transform: rotate(0deg);
}
25% {
transform: rotate(-5deg);
}
75% {
transform: rotate(5deg);
}
}
.title {
font-size: 32px;
font-weight: 700;
color: #333;
margin-bottom: 15px;
animation: fadeIn 1s ease-out 0.3s both;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.description {
font-size: 16px;
color: #666;
margin-bottom: 40px;
animation: fadeIn 1s ease-out 0.5s both;
}
.action-buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-bottom: 40px;
animation: fadeIn 1s ease-out 0.7s both;
}
.btn-primary,
.btn-secondary {
padding: 12px 30px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid #667eea;
}
.btn-secondary:hover {
background: #667eea;
color: white;
transform: translateY(-2px);
}
.btn-secondary:active {
transform: translateY(0);
}
.features {
display: flex;
justify-content: center;
gap: 30px;
animation: fadeIn 1s ease-out 0.9s both;
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.feature-icon {
font-size: 24px;
animation: bounce 2s ease-in-out infinite;
}
.feature-item:nth-child(1) .feature-icon {
animation-delay: 0s;
}
.feature-item:nth-child(2) .feature-icon {
animation-delay: 0.3s;
}
.feature-item:nth-child(3) .feature-icon {
animation-delay: 0.6s;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.background-decoration {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
overflow: hidden;
}
.circle {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
animation: float 6s ease-in-out infinite;
}
.circle-1 {
width: 300px;
height: 300px;
top: -100px;
left: -100px;
animation-delay: 0s;
}
.circle-2 {
width: 200px;
height: 200px;
bottom: -50px;
right: -50px;
animation-delay: 2s;
}
.circle-3 {
width: 150px;
height: 150px;
top: 50%;
right: 10%;
animation-delay: 4s;
}
@keyframes float {
0%, 100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(180deg);
}
}
@media (max-width: 768px) {
.login-content {
padding: 40px 20px;
}
.title {
font-size: 24px;
}
.action-buttons {
flex-direction: column;
}
.features {
flex-direction: column;
gap: 15px;
}
}
</style>

54
vite.config.js Normal file
View File

@ -0,0 +1,54 @@
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import Icons from 'unplugin-icons/vite'
import { defineConfig, loadEnv } from 'vite'
import { autoImportConfig, componentsConfig } from './config/plugins.js'
// https://vite.dev/config/
export default defineConfig(({ _command, mode }) => {
const env = loadEnv(mode, process.cwd())
console.log('Environment variables:', env)
return {
base: env.VITE_BASE_URL,
optimizeDeps: {
exclude: ['vue-element-plus-x']
},
resolve: {
alias: {
'~': fileURLToPath(new URL('./', import.meta.url)),
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
plugins: [
vue(),
autoImportConfig,
componentsConfig,
Icons({
autoInstall: true
})
],
build: {
chunkSizeWarningLimit: 2000,
outDir: 'dist',
minify: 'terser',
terserOptions: {
compress: {
keep_infinity: true,
drop_console: true,
drop_debugger: true
},
format: {
comments: false
}
},
rollupOptions: {
output: {
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
}
}
},
envPrefix: ['VITE', 'FILE']
}
})