|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
@ -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?
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
|
|
@ -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')
|
||||
}
|
||||
|
|
@ -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']
|
||||
}
|
||||
}
|
||||
|
|
@ -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()]
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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`)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './auth.js'
|
||||
export * from './captcha.js'
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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}`)
|
||||
// }
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// axios 发送ajax请求
|
||||
import request from '@/utils/request'
|
||||
|
||||
export default {
|
||||
|
||||
// 查询商品列表(对远程接口调用)
|
||||
list() {
|
||||
return request({
|
||||
url: '/pay/product/list',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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 |
|
After Width: | Height: | Size: 68 KiB |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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) => {
|
||||
// 从URL获取图片Blob对象
|
||||
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>
|
||||
|
|
@ -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
|
||||
// 从URL获取图片Blob对象
|
||||
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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export * from './display'
|
||||
export * from './param'
|
||||
export * from './user'
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
const ParamStoreSetup = () => {
|
||||
|
||||
return {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
export const useParamStore = defineStore('params', ParamStoreSetup)
|
||||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,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'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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']
|
||||
}
|
||||
})
|
||||