Compare commits

...

45 Commits

Author SHA1 Message Date
X1627315083@163.com
c8f7d157f4 3d模型优化销毁功能 2026-05-20 15:12:50 +08:00
lzp
8acb5b4ce5 12 2026-05-05 09:49:21 +08:00
lzp
1dd36b1b8c 1 2026-05-05 09:44:20 +08:00
2bca08ed97 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front 2026-05-04 17:09:00 +08:00
ba63d16d60 feat: 隐藏生成参数调节按钮 2026-05-04 17:08:58 +08:00
X1627315083@163.com
f24a9afe5c 3d 默认模型改为Normal 2026-05-04 16:57:37 +08:00
4d1d082fae style: 标签排版 2026-05-04 14:58:02 +08:00
c2f0f82218 style: 对话列表标签排版 2026-05-04 14:50:50 +08:00
620962b9ee feat: 参数标签逻辑修改&对话列表显示标签 2026-05-04 13:47:51 +08:00
89aab7e960 style: 禁用状态文字颜色 2026-05-04 11:09:20 +08:00
1078961608 feat: 选择trending report之后清除其他参数 2026-05-04 09:41:58 +08:00
d073008736 bugfix: 清除标签后同步style弹窗 2026-04-30 15:49:23 +08:00
26cd16be09 feat: 隐藏style选项的confirm按钮 2026-04-30 14:21:42 +08:00
6098993bb3 chore: 拆分input组件 2026-04-30 14:18:19 +08:00
c3d26bdb49 feat: 标签插入 2026-04-30 13:57:41 +08:00
b55a5ba896 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front 2026-04-30 10:11:20 +08:00
5097e71311 feat: trending report不再作为Placeholder 2026-04-30 10:11:19 +08:00
X1627315083@163.com
2baff3be45 生成成功加入提示 2026-04-30 10:06:45 +08:00
lzp
0b8b4c7aeb Merge branch 'main' of http://18.167.251.121:10003/aidlab/FiDA_Front 2026-04-27 16:20:23 +08:00
lzp
479b95e208 11 2026-04-27 16:20:21 +08:00
a8050f8065 bugfix: sketch图片比例 2026-04-23 09:50:41 +08:00
794b7c5fc1 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front 2026-04-22 09:43:27 +08:00
fae2e21ca6 feat: 对话过程中不允许点击versionTree 2026-04-22 09:43:24 +08:00
X1627315083@163.com
5e6d53bbe6 fix 2026-04-21 16:57:31 +08:00
X1627315083@163.com
a649f1fc63 fix 2026-04-20 15:40:41 +08:00
X1627315083@163.com
d4d9c2eede 画布删除最后一张图更新版本图 2026-04-20 11:10:58 +08:00
8cc83ae12b style: sketch视图添加白色背景 2026-04-17 17:00:00 +08:00
477c3bfa79 feat: sketch同步画布修改 2026-04-17 16:06:53 +08:00
X1627315083@163.com
6517560bd8 更新画布菜单交互逻辑,画布生成或画布编辑图片更新外面版本图片 2026-04-17 15:46:28 +08:00
X1627315083@163.com
b4fc3addb8 fix 2026-04-17 12:23:03 +08:00
25b19a3a88 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front 2026-04-16 16:40:31 +08:00
775a4f816e style: 小象助手文字统一为12px 2026-04-16 16:40:29 +08:00
X1627315083@163.com
da9527ff11 Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front 2026-04-16 09:43:09 +08:00
X1627315083@163.com
3659cb6137 fix 2026-04-16 09:43:07 +08:00
c992b6951b Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front 2026-04-15 11:24:11 +08:00
3e7d5a0403 style: 小象助手字体 2026-04-15 11:24:01 +08:00
lzp
82c10744fb Merge branch 'main' of http://18.167.251.121:10003/aidlab/FiDA_Front 2026-04-14 17:33:21 +08:00
lzp
91bf90e948 11 2026-04-14 17:33:20 +08:00
X1627315083@163.com
1f9b923128 fix 2026-04-14 16:47:22 +08:00
X1627315083@163.com
ba50072cf9 fix 2026-04-14 14:57:21 +08:00
lzp
e96eb5b30b 111 2026-04-14 14:41:00 +08:00
lzp
f0932c68db 11 2026-04-14 14:33:00 +08:00
lzp
482e7c77a1 Merge branch 'main' of http://18.167.251.121:10003/aidlab/FiDA_Front 2026-04-14 14:26:43 +08:00
lzp
85a9e79329 画布语言适配 2026-04-14 14:26:41 +08:00
X1627315083@163.com
85b47e3654 fix 2026-04-14 14:19:23 +08:00
57 changed files with 2931 additions and 1708 deletions

View File

@@ -107,6 +107,39 @@ export const toRealStyleApi = (data:toRealStyleData) => {
} }
}) })
} }
/**
* 图片转真是风格-变体
* @param data 图片转真是风格-变体的参数
* @param data.sketchId sketch id
* @param data.imageUrl 进行生成的图片。minio地址和正常地址都可以
* @param data.mode 选择的模型
* @param data.size 生成图片的大小
* @param data.userPrompt 生成图片的提示词
* @param data.original3dUrl 原始3d模型的url
* @returns 图片转真是风格
*/
export interface toRealStyleData {
sketchId?: string
imageUrl?: string
mode?: string
size?: string
userPrompt?: string
original3dUrl?: string
}
export const toRealVariantsApi = (data:toRealStyleData) => {
return request({
url: `/api/image/to-real-variants`,
method: 'post',
data:{
sketchId: data.sketchId,
imageUrl: data.imageUrl,
mode: data.mode,
size: data.size,
userPrompt: data.userPrompt,
original3dUrl: data.original3dUrl,
}
})
}
/** /**
* 线稿图上色 * 线稿图上色
* @param data 线稿图上色的参数 * @param data 线稿图上色的参数
@@ -246,3 +279,24 @@ export const threeToThreeViewsApi = (data:threeToThreeViewsData) => {
} }
}) })
} }
/**
* 根据sketchId更新VersionNode的sketch的URL值
* @param data 根据sketchId更新VersionNode的sketchL值的参数
* @param data.sketchId sketch id
* @param data.newUrl 生成的3d模型地址
* @returns 根据sketchId更新VersionNode的sketch的URL值
*/
export interface threeToThreeViewsData {
sketchId?: string
newUrl?: string
}
export const updateVersionSketchUrlApi = (data:threeToThreeViewsData) => {
return request({
url: `/api/canvas/sketchIdAndUrl/${data.sketchId}`,
method: 'put',
data:{
newUrl: data.newUrl,
}
})
}

View File

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.84839 1.55138C6.37983 1.0828 5.6203 1.08288 5.15115 1.55159C4.68298 2.01933 4.68278 2.77954 5.15158 3.24883C5.61926 3.71701 6.37934 3.71723 6.84859 3.24842C7.31704 2.7804 7.31734 2.02035 6.84839 1.55138ZM4.30303 0.702658C5.24018 -0.233608 6.75921 -0.234902 7.69694 0.70287C8.63426 1.64024 8.63456 3.16037 7.69673 4.09734C7.38215 4.41162 7.00187 4.62058 6.6 4.724V9C6.6 9.33137 6.33137 9.6 6 9.6C5.66863 9.6 5.4 9.33137 5.4 9V4.72439C4.9979 4.62112 4.61738 4.41203 4.3026 4.09692C3.36652 3.15986 3.36495 1.63989 4.30303 0.702658ZM4.1891 7.30443C4.25198 7.62979 4.0392 7.94451 3.71385 8.00738C2.86688 8.17106 2.19276 8.4162 1.74744 8.68995C1.27691 8.97919 1.2 9.20648 1.2 9.3C1.2 9.36533 1.23168 9.49297 1.43046 9.67573C1.62975 9.85897 1.95124 10.0516 2.39909 10.2257C3.29098 10.5726 4.56389 10.8 6 10.8C7.43613 10.8 8.70904 10.5726 9.60092 10.2257C10.0488 10.0516 10.3703 9.85897 10.5695 9.67573C10.7683 9.49297 10.8 9.36533 10.8 9.3C10.8 9.20648 10.7231 8.9792 10.2526 8.68995C9.80723 8.4162 9.13311 8.17106 8.28615 8.00738C7.9608 7.9445 7.74802 7.62978 7.8109 7.30443C7.87378 6.97908 8.1885 6.7663 8.51385 6.82918C9.44493 7.00912 10.2708 7.29258 10.881 7.66766C11.4659 8.02723 12 8.569 12 9.3C12 9.81457 11.7295 10.2394 11.3817 10.5591C11.0345 10.8783 10.5652 11.1383 10.0359 11.3441C8.97332 11.7573 7.54623 12 6 12C4.45378 12 3.02669 11.7573 1.96416 11.3441C1.4348 11.1383 0.965481 10.8783 0.61826 10.5591C0.270528 10.2394 0 9.81457 0 9.3C0 8.569 0.534063 8.02724 1.11901 7.66766C1.72918 7.29258 2.55506 7.00912 3.48615 6.82918C3.8115 6.7663 4.12622 6.97908 4.1891 7.30443Z" fill="#FF7A51"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,7 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.75 6.75C4.24706 6.75 4.65 7.15294 4.65 7.65C4.65 8.14706 4.24706 8.55 3.75 8.55C3.25294 8.55 2.85 8.14706 2.85 7.65C2.85 7.15294 3.25294 6.75 3.75 6.75Z" fill="#FF7A51"/>
<path d="M3.3 3.9C3.79706 3.9 4.2 4.30294 4.2 4.8C4.2 5.29706 3.79706 5.7 3.3 5.7C2.80294 5.7 2.4 5.29706 2.4 4.8C2.4 4.30294 2.80294 3.9 3.3 3.9Z" fill="#FF7A51"/>
<path d="M8.25 3.15C8.74706 3.15 9.15 3.55294 9.15 4.05C9.15 4.54706 8.74706 4.95 8.25 4.95C7.75294 4.95 7.35 4.54706 7.35 4.05C7.35 3.55294 7.75294 3.15 8.25 3.15Z" fill="#FF7A51"/>
<path d="M5.55 2.1C6.04706 2.1 6.45 2.50294 6.45 3C6.45 3.49706 6.04706 3.9 5.55 3.9C5.05294 3.9 4.65 3.49706 4.65 3C4.65 2.50294 5.05294 2.1 5.55 2.1Z" fill="#FF7A51"/>
<path d="M6 0C9.31371 0 12 2.68629 12 6C12 7.21801 11.0125 8.20547 9.79453 8.20547H7.42031C7.20907 8.2055 7.0377 8.37684 7.0377 8.58809C7.03773 8.67163 7.06507 8.75301 7.11563 8.81953L7.65703 9.53145C7.83089 9.76021 7.92534 10.0398 7.92539 10.3271C7.92539 11.2509 7.1763 12 6.25254 12H6C2.68629 12 0 9.31371 0 6C4.83208e-08 2.68629 2.68629 4.83193e-08 6 0ZM6 1.2C3.34903 1.2 1.2 3.34903 1.2 6C1.2 8.65097 3.34903 10.8 6 10.8H6.25254C6.51356 10.8 6.72539 10.5882 6.72539 10.3271C6.72534 10.3022 6.71699 10.2779 6.70195 10.258L6.16055 9.54551C5.9513 9.27019 5.83773 8.93389 5.8377 8.58809C5.8377 7.7141 6.54633 7.0055 7.42031 7.00547H9.79453C10.3498 7.00547 10.8 6.55527 10.8 6C10.8 3.34903 8.65097 1.2 6 1.2Z" fill="#FF7A51"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,10 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5159_47499)">
<path d="M0 5.49996H5.49996V0H0V5.49996ZM0.999961 0.999961H4.5V4.5H0.999961V0.999961ZM0 12H5.49996V6.50004H0V12ZM0.999961 7.5H4.5V11H0.999961V7.5ZM12 2.75004C12 1.23363 10.7664 0 9.24996 0C7.73355 0 6.50004 1.23363 6.50004 2.75004V5.50008H9.25008C10.7664 5.49996 12 4.26633 12 2.75004ZM7.5 2.75004C7.5 1.78512 8.28504 1.00008 9.24996 1.00008C10.2149 1.00008 10.9999 1.78512 10.9999 2.75004C10.9999 3.71496 10.215 4.5 9.24996 4.5H7.5V2.75004ZM6.50004 12H12V6.50004H6.50004V12ZM7.5 7.5H11V11H7.5V7.5Z" fill="#FF7A51"/>
</g>
<defs>
<clipPath id="clip0_5159_47499">
<rect width="12" height="12" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 768 B

View File

@@ -55,7 +55,7 @@
}, },
p: { p: {
style: { style: {
fontSize: '1rem', fontSize: '1.2rem',
lineHeight: 1.5 lineHeight: 1.5
} }
} }
@@ -91,7 +91,7 @@
// width: 26.2rem; // width: 26.2rem;
// align-self: end; // align-self: end;
// } // }
font-size: 1rem; font-size: 1.2rem;
font-family: 'Regular'; font-family: 'Regular';
color: #333; color: #333;
padding: 1.1rem 1.7rem; padding: 1.1rem 1.7rem;

View File

@@ -14,7 +14,7 @@
<div> <div>
<button class="export" @click="emit('export')"> <button class="export" @click="emit('export')">
<span class="icon"><svg-icon name="export" size="11" /></span> <span class="icon"><svg-icon name="export" size="11" /></span>
<span class="text">Export</span> <span class="text">{{ $t('FlowCanvas.export') }}</span>
</button> </button>
<div v-loading="true" class="mask" v-if="downloadData.status == 'loading'"></div> <div v-loading="true" class="mask" v-if="downloadData.status == 'loading'"></div>
</div> </div>

View File

@@ -1,12 +1,12 @@
<template> <template>
<!-- 添加印花 --> <!-- 添加印花 -->
<div class="add-print"> <div class="add-print">
<p class="label">Print</p> <p class="label">{{ $t('FlowCanvas.print') }}</p>
<upload-file v-model="data.file" /> <upload-file v-model="data.file" />
<p class="label">Settings</p> <p class="label">{{ $t('FlowCanvas.settings') }}</p>
<div class="settings"> <div class="settings">
<div> <div>
<p class="label">Angle</p> <p class="label">{{ $t('FlowCanvas.angle') }}</p>
<my-input <my-input
v-model="data.setting.angle" v-model="data.setting.angle"
type="number" type="number"
@@ -18,7 +18,7 @@
/> />
</div> </div>
<div> <div>
<span class="label">Scale</span> <span class="label">{{ $t('FlowCanvas.scale') }}</span>
<slider <slider
:min="1" :min="1"
:max="1000" :max="1000"
@@ -27,7 +27,7 @@
/> />
</div> </div>
<div> <div>
<span class="label">Gap X</span> <span class="label">{{ $t('FlowCanvas.gapX') }}</span>
<slider <slider
:min="0" :min="0"
:max="1000" :max="1000"
@@ -36,7 +36,7 @@
/> />
</div> </div>
<div> <div>
<span class="label">Gap Y</span> <span class="label">{{ $t('FlowCanvas.gapY') }}</span>
<slider <slider
:min="0" :min="0"
:max="1000" :max="1000"
@@ -45,14 +45,14 @@
/> />
</div> </div>
<div> <div>
<span class="label">Offset</span> <span class="label">{{ $t('FlowCanvas.offset') }}</span>
<offset-tool v-model="data.setting.offset" :show-dish="false" /> <offset-tool v-model="data.setting.offset" :show-dish="false" />
</div> </div>
<div class="offset"> <div class="offset">
<offset-tool v-model="data.setting.offset" :show-input="false" /> <offset-tool v-model="data.setting.offset" :show-input="false" />
</div> </div>
</div> </div>
<p class="label">Prompt</p> <p class="label">{{ $t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" /> <my-textarea v-model="data.prompt" />
</div> </div>
</template> </template>

View File

@@ -12,7 +12,7 @@
" "
> >
<span class="icon"> <span class="icon">
<svg-icon :name="v.type + '-2'" size="15" size-unit="px" /> <svg-icon :name="v.type.split('_')[0] + '-2'" size="15" size-unit="px" />
</span> </span>
<span class="title">{{ v.title }}</span> <span class="title">{{ v.title }}</span>
</div> </div>
@@ -22,6 +22,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, inject, computed } from 'vue' import { ref, inject, computed } from 'vue'
import { NODE_DATATYPE, NODE_DATATIER } from '../../../tools/index.d' import { NODE_DATATYPE, NODE_DATATIER } from '../../../tools/index.d'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const nodeManager = inject('nodeManager') as any const nodeManager = inject('nodeManager') as any
const stateManager = inject('stateManager') as any const stateManager = inject('stateManager') as any
const props = defineProps({ const props = defineProps({
@@ -35,29 +37,48 @@
{ {
tier: NODE_DATATIER.TO_REAL_STYLE, tier: NODE_DATATIER.TO_REAL_STYLE,
type: NODE_DATATYPE.TO_REAL_STYLE, type: NODE_DATATYPE.TO_REAL_STYLE,
title: 'To Real Style' title: t('FlowCanvas.toRealStyleTitle')
}, },
{ {
tier: NODE_DATATIER.TO_REAL_VARIANTS, tier: NODE_DATATIER.TO_REAL_VARIANTS,
type: NODE_DATATYPE.TO_REAL_VARIANTS, type: NODE_DATATYPE.TO_REAL_VARIANTS,
title: 'To Real Variants' title: t('FlowCanvas.toRealVariantsTitle')
}, },
{ {
tier: NODE_DATATIER.SURFACE_EDIT, tier: NODE_DATATIER.SURFACE_EDIT,
type: NODE_DATATYPE.SURFACE_EDIT, type: NODE_DATATYPE.SURFACE_EDIT,
title: 'Surface Edit', title: t('FlowCanvas.surfaceEditTitle'),
secondaryMenu: { secondaryMenu: {
title: 'Surface Edit', title: t('FlowCanvas.surfaceEditTitle'),
icon: NODE_DATATYPE.SURFACE_EDIT, icon: NODE_DATATYPE.SURFACE_EDIT,
selectList: [ selectList: [
{ {
tier: NODE_DATATIER.CANVAS_MODE, tier: NODE_DATATIER.CANVAS_MODE,
type: NODE_DATATYPE.CANVAS_MODE, type: NODE_DATATYPE.CANVAS_MODE,
title: 'Surface Edit (Canvas)', title: t('FlowCanvas.surfaceEditCanvasTitle'),
},{ },{
tier: NODE_DATATIER.Fast_MODE, tier: NODE_DATATIER.Fast_MODE,
type: NODE_DATATYPE.Fast_MODE, type: NODE_DATATYPE.Fast_MODE,
title: 'Surface Edit', title: t('FlowCanvas.surfaceEditTitle'),
},
]
}
},{
tier: NODE_DATATIER.SURFACE_EDIT_,
type: NODE_DATATYPE.SURFACE_EDIT_,
title: t('FlowCanvas.surfaceEditTitle'),
secondaryMenu: {
title: t('FlowCanvas.surfaceEditTitle'),
icon: NODE_DATATYPE.SURFACE_EDIT_,
selectList: [
{
tier: NODE_DATATIER.CANVAS_MODE_,
type: NODE_DATATYPE.CANVAS_MODE_,
title: t('FlowCanvas.surfaceEditCanvasTitle'),
},{
tier: NODE_DATATIER.Fast_MODE_,
type: NODE_DATATYPE.Fast_MODE_,
title: t('FlowCanvas.surfaceEditTitle'),
}, },
] ]
} }
@@ -65,22 +86,22 @@
{ {
tier: NODE_DATATIER.SCENE_COMPOSITION, tier: NODE_DATATIER.SCENE_COMPOSITION,
type: NODE_DATATYPE.SCENE_COMPOSITION, type: NODE_DATATYPE.SCENE_COMPOSITION,
title: 'Scene Composition' title: t('FlowCanvas.sceneCompositionTitle')
}, },
{ {
tier: NODE_DATATIER.COLOR_PALETTE, tier: NODE_DATATIER.COLOR_PALETTE,
type: NODE_DATATYPE.COLOR_PALETTE, type: NODE_DATATYPE.COLOR_PALETTE,
title: 'Color Palette' title: t('FlowCanvas.colorPaletteTitle')
}, },
{ {
tier: NODE_DATATIER.TO_3D_MODEL, tier: NODE_DATATIER.TO_3D_MODEL,
type: NODE_DATATYPE.TO_3D_MODEL, type: NODE_DATATYPE.TO_3D_MODEL,
title: 'To 3D Model' title: t('FlowCanvas.to3DModelTitle')
}, },
{ {
tier: NODE_DATATIER.TO_3VIEW, tier: NODE_DATATIER.TO_3VIEW,
type: NODE_DATATYPE.TO_3VIEW, type: NODE_DATATYPE.TO_3VIEW,
title: 'To 3-View' title: t('FlowCanvas.to3ViewTitle')
} }
]) ])
const onClickItem = (v) => { const onClickItem = (v) => {

View File

@@ -1,9 +1,9 @@
<template> <template>
<!-- 颜色调色板 --> <!-- 颜色调色板 -->
<div class="color-palette"> <div class="color-palette">
<p class="label">Mode</p> <p class="label">{{ $t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" /> <my-select v-model="data.mode" :list="modeList" />
<p class="label">Choose Color</p> <p class="label">{{ $t('FlowCanvas.chooseColor') }}</p>
<div class="color-list"> <div class="color-list">
<div <div
class="color-item" class="color-item"
@@ -25,6 +25,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, onMounted, ref } from 'vue' import { reactive, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const data = reactive({ const data = reactive({
colors: [], colors: [],
mode: 'Advanced', mode: 'Advanced',
@@ -45,8 +47,8 @@
data.colors.push(target.value) data.colors.push(target.value)
} }
const modeList = ref([ const modeList = ref([
{ value: 'Advanced', label: 'Advanced' }, { value: 'Advanced', label: t('FlowCanvas.advancedMode') },
{ value: 'Normal', label: 'Normal' } { value: 'Normal', label: t('FlowCanvas.normalMode') }
]) ])
const getApiData = ()=>{ const getApiData = ()=>{
return { return {

View File

@@ -1,7 +1,7 @@
<template> <template>
<!-- 画布编辑印花 --> <!-- 画布编辑印花 -->
<div class="fast-mode"> <div class="fast-mode">
<p class="label">Output</p> <p class="label">{{ $t('FlowCanvas.output') }}</p>
<div class="imgBox"> <div class="imgBox">
<img :src="data.url" alt=""> <img :src="data.url" alt="">
</div> </div>
@@ -13,6 +13,7 @@
import myEvent from '@/utils/myEvent' import myEvent from '@/utils/myEvent'
import { getCurrentTime } from '../../../../tools/tools.ts' import { getCurrentTime } from '../../../../tools/tools.ts'
import { NODE_DATATIER } from '../../../tools/index.d' import { NODE_DATATIER } from '../../../tools/index.d'
import { updateVersionSketchUrlApi } from '@/api/flow-canvas'
const attrs = useAttrs() const attrs = useAttrs()
const stateManager = inject('stateManager') as any const stateManager = inject('stateManager') as any
const nodeManager = inject('nodeManager') as any const nodeManager = inject('nodeManager') as any
@@ -24,7 +25,7 @@
return { return {
} }
} }
const opCanvas = ()=>{ const opCanvas = (tier)=>{
const superiorNodeUrl = stateManager.getSuperiorNodeImage(attrs?.node?.data?.superiorID || null) const superiorNodeUrl = stateManager.getSuperiorNodeImage(attrs?.node?.data?.superiorID || null)
if (!superiorNodeUrl) console.log('superiorNodeUrl 找不到原始图片') if (!superiorNodeUrl) console.log('superiorNodeUrl 找不到原始图片')
const data = { const data = {
@@ -46,7 +47,7 @@
superiorID: attrs.node.id, superiorID: attrs.node.id,
superiorNodeType: attrs.node?.data?.type, superiorNodeType: attrs.node?.data?.type,
createIndexPosition: 0 + subordNodes.length, createIndexPosition: 0 + subordNodes.length,
tier: NODE_DATATIER.RESULT_IMAGE, tier,
isActive: subordNodes.length == 0, isActive: subordNodes.length == 0,
data: { data: {
imageProcessTasks:[workbenchData], imageProcessTasks:[workbenchData],
@@ -54,6 +55,15 @@
} }
} }
}) })
//更新VersionNode的sketchIDAndUrl中指定key的URL值
updateVersionSketchUrlApi({
sketchId: stateManager.sketchId.value,
newUrl: options.url,
}).then(()=>{
//更新VersionNode的versionImgUpdataList中指定key的URL值
let initialNode = stateManager.getInitialNode()
initialNode.data.versionImgUpdataList.push(options.url)
})
}, },
} }
eventManager.removeEvents() eventManager.removeEvents()

View File

@@ -3,7 +3,7 @@
<div class="header"> <div class="header">
<svg-icon <svg-icon
v-if="attrs.node?.data?.secondaryMenu?.icon || !currentComponent?.hideIcon" v-if="attrs.node?.data?.secondaryMenu?.icon || !currentComponent?.hideIcon"
:name="attrs.node?.data?.secondaryMenu?.icon || currentComponent?.type" :name="attrs.node?.data?.secondaryMenu?.icon || currentComponent?.type.split('_')[0]"
color="#fff" color="#fff"
size="16" size="16"
size-unit="px" size-unit="px"
@@ -19,13 +19,13 @@
<div class="footer" @mousedown.stop v-if="!currentComponent?.hideFooter"> <div class="footer" @mousedown.stop v-if="!currentComponent?.hideFooter">
<button @click="onGenerateClick"> <button @click="onGenerateClick">
<svg-icon name="xingxing" size="16" size-unit="px" /> <svg-icon name="xingxing" size="16" size-unit="px" />
<span>Generate</span> <span>{{ $t('FlowCanvas.generate') }}</span>
</button> </button>
</div> </div>
<div class="footer canvasEdit" @mousedown.stop v-if="currentComponent?.showCanvasEdit"> <div class="footer canvasEdit" @mousedown.stop v-if="currentComponent?.showCanvasEdit">
<button @click="currentComponent?.on()"> <button @click="currentComponent?.on()">
<svg-icon name="xingxing" size="16" size-unit="px" /> <svg-icon name="xingxing" size="16" size-unit="px" />
<span>Edit</span> <span>{{ $t('FlowCanvas.edit') }}</span>
</button> </button>
</div> </div>
</div> </div>
@@ -46,7 +46,7 @@
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { ElMessageBox } from 'element-plus' import { ElMessageBox } from 'element-plus'
import { toRealStyleApi, toColorPaletteApi, toSceneCompositionApi, sketchAddPrintApi, sketchToThreeApi, threeToThreeViewsApi } from '@/api/flow-canvas' import { toRealStyleApi, toColorPaletteApi, toSceneCompositionApi, sketchAddPrintApi, sketchToThreeApi, threeToThreeViewsApi, toRealVariantsApi } from '@/api/flow-canvas'
// import ToVideo from './to-video.vue' // import ToVideo from './to-video.vue'
// import AddPrint from './add-print.vue' // import AddPrint from './add-print.vue'
@@ -58,7 +58,7 @@
{ {
tier: NODE_DATATIER.CARDS_SELECT, tier: NODE_DATATIER.CARDS_SELECT,
type: NODE_DATATYPE.CARDS_SELECT, type: NODE_DATATYPE.CARDS_SELECT,
title: 'Advanced Tools', title: t('FlowCanvas.selectCardsTitle'),
component: CardsSelect, component: CardsSelect,
hideFooter: true, hideFooter: true,
hideIcon: true, hideIcon: true,
@@ -66,60 +66,78 @@
{ {
tier: NODE_DATATIER.TO_REAL_STYLE, tier: NODE_DATATIER.TO_REAL_STYLE,
type: NODE_DATATYPE.TO_REAL_STYLE, type: NODE_DATATYPE.TO_REAL_STYLE,
title: 'To Real Style', title: t('FlowCanvas.toRealStyleTitle'),
component: ToRealStyle, component: ToRealStyle,
api: toRealStyleApi api: toRealStyleApi
}, },
{ {
tier: NODE_DATATIER.TO_REAL_VARIANTS, tier: NODE_DATATIER.TO_REAL_VARIANTS,
type: NODE_DATATYPE.TO_REAL_VARIANTS, type: NODE_DATATYPE.TO_REAL_VARIANTS,
title: 'To Real Variants', title: t('FlowCanvas.toRealVariantsTitle'),
component: ToRealVariants, component: ToRealVariants,
api: toRealStyleApi api: toRealVariantsApi
}, },
{ {
tier: NODE_DATATIER.Fast_MODE, tier: NODE_DATATIER.Fast_MODE,
type: NODE_DATATYPE.Fast_MODE, type: NODE_DATATYPE.Fast_MODE,
title: 'Surface Edit', title: t('FlowCanvas.surfaceEditTitle'),
component: SurfaceEdit, component: SurfaceEdit,
api: sketchAddPrintApi api: sketchAddPrintApi
}, },
{ {
tier: NODE_DATATIER.CANVAS_MODE, tier: NODE_DATATIER.CANVAS_MODE,
type: NODE_DATATYPE.CANVAS_MODE, type: NODE_DATATYPE.CANVAS_MODE,
title: 'Surface Edit (Canvas)', title: t('FlowCanvas.surfaceEditCanvasTitle'),
component: FastMode, component: FastMode,
hideFooter: true, hideFooter: true,
showCanvasEdit: true, showCanvasEdit: true,
on: ()=>{ on: ()=>{
componentRef.value?.opCanvas() componentRef.value?.opCanvas(0)
}
},
{
tier: NODE_DATATIER.Fast_MODE_,
type: NODE_DATATYPE.Fast_MODE_,
title: t('FlowCanvas.surfaceEditTitle'),
component: SurfaceEdit,
api: sketchAddPrintApi
},
{
tier: NODE_DATATIER.CANVAS_MODE_,
type: NODE_DATATYPE.CANVAS_MODE_,
title: t('FlowCanvas.surfaceEditCanvasTitle'),
component: FastMode,
hideFooter: true,
showCanvasEdit: true,
on: ()=>{
componentRef.value?.opCanvas(1)
} }
}, },
{ {
tier: NODE_DATATIER.SCENE_COMPOSITION, tier: NODE_DATATIER.SCENE_COMPOSITION,
type: NODE_DATATYPE.SCENE_COMPOSITION, type: NODE_DATATYPE.SCENE_COMPOSITION,
title: 'Scene Composition', title: t('FlowCanvas.sceneCompositionTitle'),
component: SceneComposition, component: SceneComposition,
api: toSceneCompositionApi api: toSceneCompositionApi
}, },
{ {
tier: NODE_DATATIER.COLOR_PALETTE, tier: NODE_DATATIER.COLOR_PALETTE,
type: NODE_DATATYPE.COLOR_PALETTE, type: NODE_DATATYPE.COLOR_PALETTE,
title: 'Color Palette', title: t('FlowCanvas.colorPaletteTitle'),
component: ColorPalette, component: ColorPalette,
api: toColorPaletteApi api: toColorPaletteApi
}, },
{ {
tier: NODE_DATATIER.TO_3D_MODEL, tier: NODE_DATATIER.TO_3D_MODEL,
type: NODE_DATATYPE.TO_3D_MODEL, type: NODE_DATATYPE.TO_3D_MODEL,
title: 'To 3D Model', title: t('FlowCanvas.to3DModelTitle'),
component: To3DModel, component: To3DModel,
api:sketchToThreeApi api:sketchToThreeApi
}, },
{ {
tier: NODE_DATATIER.TO_3VIEW, tier: NODE_DATATIER.TO_3VIEW,
type: NODE_DATATYPE.TO_3VIEW, type: NODE_DATATYPE.TO_3VIEW,
title: 'To 3-View', title: t('FlowCanvas.to3ViewTitle'),
component: To3View, component: To3View,
api:threeToThreeViewsApi api:threeToThreeViewsApi
} }
@@ -155,6 +173,7 @@
const onGenerateClick = async () => { const onGenerateClick = async () => {
const data = componentRef.value?.getApiData?.() || {} const data = componentRef.value?.getApiData?.() || {}
const subordNodes = stateManager.getSubordNodes(attrs.node.id) const subordNodes = stateManager.getSubordNodes(attrs.node.id)
const maxPosition = subordNodes.length == 0?0:(subordNodes[subordNodes.length - 1].data.createIndexPosition + 1)
const superiorNodeUrl = stateManager.getSuperiorNodeImage(attrs.node.data.superiorID) const superiorNodeUrl = stateManager.getSuperiorNodeImage(attrs.node.data.superiorID)
if(!superiorNodeUrl)return console.log('superiorNodeUrl 找不到原始图片') if(!superiorNodeUrl)return console.log('superiorNodeUrl 找不到原始图片')
emit('update-data', componentRef.value?.data) emit('update-data', componentRef.value?.data)
@@ -175,12 +194,16 @@
NODE_DATATYPE.CANVAS_MODE, NODE_DATATYPE.CANVAS_MODE,
NODE_DATATYPE.SCENE_COMPOSITION, NODE_DATATYPE.SCENE_COMPOSITION,
NODE_DATATYPE.COLOR_PALETTE, NODE_DATATYPE.COLOR_PALETTE,
NODE_DATATYPE.Fast_MODE_,
NODE_DATATYPE.CANVAS_MODE_,
] ]
let tritList = [ let tritList = [
NODE_DATATIER.Fast_MODE, NODE_DATATIER.Fast_MODE,
NODE_DATATIER.CANVAS_MODE, NODE_DATATIER.CANVAS_MODE,
NODE_DATATIER.SCENE_COMPOSITION, NODE_DATATIER.SCENE_COMPOSITION,
NODE_DATATIER.COLOR_PALETTE, NODE_DATATIER.COLOR_PALETTE,
NODE_DATATIER.Fast_MODE_,
NODE_DATATIER.CANVAS_MODE_,
] ]
let tier = (tritList.includes(currentComponent.value.tier) && typeList.includes(currentComponent.value.type))?currentComponent.value.tier - 1:currentComponent.value.tier let tier = (tritList.includes(currentComponent.value.tier) && typeList.includes(currentComponent.value.type))?currentComponent.value.tier - 1:currentComponent.value.tier
if(NODE_DATATYPE.TO_REAL_STYLE == currentComponent.value.type && false){ if(NODE_DATATYPE.TO_REAL_STYLE == currentComponent.value.type && false){
@@ -206,7 +229,7 @@
superiorID: attrs.node.id, superiorID: attrs.node.id,
superiorNodeType: attrs.node?.data?.type, superiorNodeType: attrs.node?.data?.type,
superiorGenerateImg: superiorNodeUrl || null, superiorGenerateImg: superiorNodeUrl || null,
createIndexPosition: index + subordNodes.length, createIndexPosition: index + maxPosition,
tier: tier, tier: tier,
isActive: index == 0 && subordNodes.length == 0, isActive: index == 0 && subordNodes.length == 0,
data: { data: {

View File

@@ -1,11 +1,11 @@
<template> <template>
<!-- 场景构图 --> <!-- 场景构图 -->
<div class="scene-composition"> <div class="scene-composition">
<p class="label">Prompt</p> <p class="label">{{ $t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" /> <my-textarea v-model="data.prompt" />
<p class="label">Mode</p> <p class="label">{{ $t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" /> <my-select v-model="data.mode" :list="modeList" />
<p class="label">Choose Style</p> <p class="label">{{ $t('FlowCanvas.chooseStyle') }}</p>
<div class="style-list"> <div class="style-list">
<div <div
class="item" class="item"
@@ -26,26 +26,28 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, reactive, onMounted } from 'vue' import { computed, ref, reactive, onMounted } from 'vue'
import myTextarea from '../../tools/my-textarea.vue' import myTextarea from '../../tools/my-textarea.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const styleList = ref([ const styleList = ref([
{ label: 'Colorful', value: 'Colorful' }, { label: t('FlowCanvas.colorful'), value: 'Colorful' },
{ label: 'Minimalist', value: 'Minimalist' }, { label: t('FlowCanvas.minimalist'), value: 'Minimalist' },
{ label: 'Modernist', value: 'Modernist' }, { label: t('FlowCanvas.modernist'), value: 'Modernist' },
{ label: 'Bauhaus', value: 'Bauhaus' }, { label: t('FlowCanvas.bauhaus'), value: 'Bauhaus' },
{ label: 'Mintage', value: 'Mintage' }, { label: t('FlowCanvas.mintage'), value: 'Mintage' },
{ label: 'Industrial', value: 'Industrial' }, { label: t('FlowCanvas.industrial'), value: 'Industrial' },
{ label: 'Futuristic', value: 'Futuristic' }, { label: t('FlowCanvas.futuristic'), value: 'Futuristic' },
{ label: 'Elegant', value: 'Elegant' }, { label: t('FlowCanvas.elegant'), value: 'Elegant' },
{ label: 'Organic', value: 'Organic' }, { label: t('FlowCanvas.organic'), value: 'Organic' },
{ label: 'Calm', value: 'Calm' }, { label: t('FlowCanvas.calm'), value: 'Calm' },
{ label: 'Abstract', value: 'Abstract' }, { label: t('FlowCanvas.abstract'), value: 'Abstract' },
{ label: 'Kitsch-core', value: 'Kitsch-core' }, { label: t('FlowCanvas.kitschCore'), value: 'Kitsch-core' },
{ label: 'Sophisticated', value: 'Sophisticated' }, { label: t('FlowCanvas.sophisticated'), value: 'Sophisticated' },
{ label: 'Maximalism', value: 'Maximalism' }, { label: t('FlowCanvas.maximalism'), value: 'Maximalism' },
{ label: 'Clean', value: 'Clean' }, { label: t('FlowCanvas.clean'), value: 'Clean' },
{ label: 'Bright Colors', value: 'Bright Colors' }, { label: t('FlowCanvas.brightColors'), value: 'Bright Colors' },
{ label: 'Luxurious', value: 'Luxurious' }, { label: t('FlowCanvas.luxurious'), value: 'Luxurious' },
{ label: 'Bold Colors', value: 'Bold Colors' }, { label: t('FlowCanvas.boldColors'), value: 'Bold Colors' },
{ label: 'Brutalism', value: 'Brutalism' } { label: t('FlowCanvas.brutalism'), value: 'Brutalism' }
]) ])
const data = reactive({ const data = reactive({
prompt: '', prompt: '',
@@ -53,8 +55,8 @@
mode: 'Advanced', mode: 'Advanced',
}) })
const modeList = ref([ const modeList = ref([
{ value: 'Advanced', label: 'Advanced' }, { value: 'Advanced', label: t('FlowCanvas.advancedMode') },
{ value: 'Normal', label: 'Normal' } { value: 'Normal', label: t('FlowCanvas.normalMode') },
]) ])
const onClickStyle = (value: string) => { const onClickStyle = (value: string) => {
if (data.styles.includes(value)) { if (data.styles.includes(value)) {

View File

@@ -1,11 +1,11 @@
<template> <template>
<!-- 编辑素材 --> <!-- 编辑素材 -->
<div class="surface-edit"> <div class="surface-edit">
<p class="label">Image</p> <p class="label">{{ $t('FlowCanvas.image') }}</p>
<upload-file v-model="data.file" /> <upload-file v-model="data.file" />
<p class="label">Mode</p> <p class="label">{{ $t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" /> <my-select v-model="data.mode" :list="modeList" />
<p class="label">Prompt</p> <p class="label">{{ $t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" /> <my-textarea v-model="data.prompt" />
</div> </div>
</template> </template>
@@ -15,14 +15,16 @@
import myTextarea from '../../tools/my-textarea.vue' import myTextarea from '../../tools/my-textarea.vue'
import uploadFile from '../../tools/upload-file.vue' import uploadFile from '../../tools/upload-file.vue'
import mySelect from '../../tools/my-select.vue' import mySelect from '../../tools/my-select.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const data = reactive({ const data = reactive({
prompt: '', prompt: '',
file: null, file: null,
mode: 'Advanced', mode: 'Advanced',
}) })
const modeList = ref([ const modeList = ref([
{ value: 'Advanced', label: 'Advanced' }, { value: 'Advanced', label: t('FlowCanvas.advancedMode') },
{ value: 'Normal', label: 'Normal' } { value: 'Normal', label: t('FlowCanvas.normalMode') }
]) ])
const getApiData = ()=>{ const getApiData = ()=>{
return { return {

View File

@@ -1,11 +1,11 @@
<template> <template>
<!-- 转3D模型 --> <!-- 转3D模型 -->
<div class="to-3d-model"> <div class="to-3d-model">
<p class="label">Image</p> <p class="label">{{ $t('FlowCanvas.image') }}</p>
<div class="image"> <div class="image">
<img :src="data.url" alt=""> <img :src="data.url" alt="">
</div> </div>
<p class="label">Mode</p> <p class="label">{{ $t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" /> <my-select v-model="data.mode" :list="modeList" />
</div> </div>
</template> </template>
@@ -13,15 +13,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, inject, useAttrs, computed,ref } from 'vue' import { reactive, inject, useAttrs, computed,ref } from 'vue'
import uploadFile from '../../tools/upload-file.vue' import uploadFile from '../../tools/upload-file.vue'
import mySelect from '../../tools/my-select.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const attrs = useAttrs() const attrs = useAttrs()
const stateManager = inject('stateManager') as any const stateManager = inject('stateManager') as any
const data = reactive({ const data = reactive({
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)), url: stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID),
mode: 'Advanced', mode: 'Normal',
}) })
const modeList = ref([ const modeList = ref([
{ value: 'Advanced', label: 'Advanced' }, { value: 'Advanced', label: t('FlowCanvas.advancedMode') },
{ value: 'Normal', label: 'Normal' } { value: 'Normal', label: t('FlowCanvas.normalMode') }
]) ])
const getApiData = ()=>{ const getApiData = ()=>{
return { return {

View File

@@ -1,7 +1,7 @@
<template> <template>
<!-- 转3-View --> <!-- 转3-View -->
<div class="to-3view"> <div class="to-3view">
<p class="label">3D Model</p> <p class="label">{{ $t('FlowCanvas._3DModel') }}</p>
<div class="image"> <div class="image">
<img :src="data.url" alt=""> <img :src="data.url" alt="">
</div> </div>

View File

@@ -1,9 +1,9 @@
<template> <template>
<!-- 转CAD --> <!-- 转CAD -->
<div class="to-cad"> <div class="to-cad">
<p class="label">3D Model</p> <p class="label">{{ $t('FlowCanvas._3DModel') }}</p>
<upload-file v-model="data.file" /> <upload-file v-model="data.file" />
<p class="label">Prompt</p> <p class="label">{{ $t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" /> <my-textarea v-model="data.prompt" />
</div> </div>
</template> </template>

View File

@@ -1,7 +1,7 @@
<template> <template>
<!-- 转换为真实图 --> <!-- 转换为真实图 -->
<div class="to-real-style"> <div class="to-real-style">
<p class="label">Prompt</p> <p class="label">{{ $t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" /> <my-textarea v-model="data.prompt" />
<div class="shortcut-list"> <div class="shortcut-list">
<div <div
@@ -13,9 +13,9 @@
{{ v.label }} {{ v.label }}
</div> </div>
</div> </div>
<p class="label">Mode</p> <p class="label">{{ $t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" /> <my-select v-model="data.mode" :list="modeList" />
<p class="label">Size</p> <p class="label">{{ $t('FlowCanvas.size') }}</p>
<pixel-ratio-selection v-model="data.pixelRatio" /> <pixel-ratio-selection v-model="data.pixelRatio" />
</div> </div>
</template> </template>
@@ -25,43 +25,30 @@
import myTextarea from '../../tools/my-textarea.vue' import myTextarea from '../../tools/my-textarea.vue'
import mySelect from '../../tools/my-select.vue' import mySelect from '../../tools/my-select.vue'
import pixelRatioSelection from '../../tools/pixel-ratio-selection.vue' import pixelRatioSelection from '../../tools/pixel-ratio-selection.vue'
const shortcutList = ref([ import { useI18n } from 'vue-i18n'
{ const { t } = useI18n()
label: 'Change the...', const shortcutList = ref([])
value: 'Change the style to a realistic design. ' for (let i = 0; i < 5; i++) {
}, shortcutList.value.push({
{ label: t(`FlowCanvas.toRealStyleShortcut${i + 1}Label`),
label: 'Bright Colors...', value: t(`FlowCanvas.toRealStyleShortcut${i + 1}Value`)
value: 'Bright colors with modern patterns, change the style to a realistic furniture design. ' })
},
{
label: 'Make the...',
value: 'Make the structure more refined and balanced, change the style to a realistic furniture style. '
},
{
label: 'Imagine...',
value: 'Imagine this furniture with detailed fabric textures, change the style to a realistic design. '
},
{
label: 'Wood Materials with...',
value: 'Wood materials with natural oak texture and soft fabric, change the style to a realistic furniture design.'
} }
])
const modeList = ref([ const modeList = ref([
{ value: 'Advanced', label: 'Advanced' }, { value: 'Advanced', label: t('FlowCanvas.advancedMode') },
{ value: 'Normal', label: 'Normal' } { value: 'Normal', label: t('FlowCanvas.normalMode') }
]) ])
const data = reactive({ const data = reactive({
prompt: '', prompt: '',
pixelRatio: '1:1', pixelRatio: '1:1',
mode: 'Advanced', mode: 'Advanced'
}) })
const getApiData = () => { const getApiData = () => {
return { return {
mode: data.mode, mode: data.mode,
size: data.pixelRatio, size: data.pixelRatio,
userPrompt: data.prompt, userPrompt: data.prompt
} }
} }

View File

@@ -1,8 +1,8 @@
<template> <template>
<!-- 转换为真实图 --> <!-- 转换为真实图 -->
<div class="to-real-style"> <div class="to-real-style">
<p class="label">Prompt</p> <p class="label">{{ t('FlowCanvas.prompt') }}</p>
<my-textarea v-model="data.prompt" :placeholder="$t('flowCanvas.toRealVariantsPlaceholder')" /> <my-textarea v-model="data.prompt" :placeholder="$t('FlowCanvas.toRealVariantsPlaceholder')" />
<div class="shortcut-list"> <div class="shortcut-list">
<div <div
class="item" class="item"
@@ -13,9 +13,9 @@
{{ v.label }} {{ v.label }}
</div> </div>
</div> </div>
<p class="label">Mode</p> <!-- <p class="label">{{ t('FlowCanvas.mode') }}</p>
<my-select v-model="data.mode" :list="modeList" /> <my-select v-model="data.mode" :list="modeList" /> -->
<p class="label">Size</p> <p class="label">{{ t('FlowCanvas.size') }}</p>
<pixel-ratio-selection v-model="data.pixelRatio" /> <pixel-ratio-selection v-model="data.pixelRatio" />
</div> </div>
</template> </template>
@@ -29,24 +29,24 @@
const { t } = useI18n() const { t } = useI18n()
const shortcutList = ref([ const shortcutList = ref([
{ {
label: t('flowCanvas.toRealVariantsShortcut1Label'), label: t('FlowCanvas.toRealVariantsShortcut1Label'),
value: t('flowCanvas.toRealVariantsShortcut1Value') value: t('FlowCanvas.toRealVariantsShortcut1Value')
}, },
{ {
label: t('flowCanvas.toRealVariantsShortcut2Label'), label: t('FlowCanvas.toRealVariantsShortcut2Label'),
value: t('flowCanvas.toRealVariantsShortcut2Value') value: t('FlowCanvas.toRealVariantsShortcut2Value')
}, },
{ {
label: t('flowCanvas.toRealVariantsShortcut3Label'), label: t('FlowCanvas.toRealVariantsShortcut3Label'),
value: t('flowCanvas.toRealVariantsShortcut3Value') value: t('FlowCanvas.toRealVariantsShortcut3Value')
}, },
{ {
label: t('flowCanvas.toRealVariantsShortcut4Label'), label: t('FlowCanvas.toRealVariantsShortcut4Label'),
value: t('flowCanvas.toRealVariantsShortcut4Value') value: t('FlowCanvas.toRealVariantsShortcut4Value')
}, },
{ {
label: t('flowCanvas.toRealVariantsShortcut5Label'), label: t('FlowCanvas.toRealVariantsShortcut5Label'),
value: t('flowCanvas.toRealVariantsShortcut5Value') value: t('FlowCanvas.toRealVariantsShortcut5Value')
} }
]) ])
const stateManager = inject('stateManager') as any const stateManager = inject('stateManager') as any
@@ -68,7 +68,7 @@
mode: data.mode, mode: data.mode,
size: data.pixelRatio, size: data.pixelRatio,
userPrompt: data.prompt, userPrompt: data.prompt,
aaa: superior?.data?.superiorGenerateImg, original3dUrl: superior?.data?.superiorGenerateImg,
} }
} }
@@ -80,7 +80,7 @@
> .shortcut-list { > .shortcut-list {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px 4px; gap: 10px 2px;
user-select: none; user-select: none;
> .item { > .item {
display: flex; display: flex;

View File

@@ -19,7 +19,7 @@
</span> </span>
<button class="edit" @click="onEdit(item)" v-if="node.data.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL || node.data.tier == 0"> <button class="edit" @click="onEdit(item)" v-if="node.data.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL || node.data.tier == 0">
<span class="icon"><svg-icon name="edit" size="13" /></span> <span class="icon"><svg-icon name="edit" size="13" /></span>
<span class="text">Edit</span> <span class="text">{{ $t('FlowCanvas.edit') }}</span>
</button> </button>
</div> </div>
<img <img
@@ -65,8 +65,11 @@
import { reactive, ref, onBeforeUnmount, useAttrs, inject, watch, computed, onMounted } from 'vue' import { reactive, ref, onBeforeUnmount, useAttrs, inject, watch, computed, onMounted } from 'vue'
import HighlightAdmin from '@/components/highlightAdmin.vue' import HighlightAdmin from '@/components/highlightAdmin.vue'
import { NODE_DATATIER, NODE_DATATYPE } from '../../tools/index.d' import { NODE_DATATIER, NODE_DATATYPE } from '../../tools/index.d'
import { useI18n } from 'vue-i18n'
import { updateVersionSketchUrlApi } from '@/api/flow-canvas'
const { t } = useI18n()
const openImagePreview = inject('openImagePreview') as (url: string) => void const openImagePreview = inject('openImagePreview') as (url: string) => void
const openThreeModelPreview = inject('openThreeModelPreview') as (url: string) => void const openThreeModelPreview = inject('openThreeModelPreview') as (obj: any) => void
const props = defineProps({ const props = defineProps({
node: { node: {
type: Object, type: Object,
@@ -125,9 +128,9 @@
{ immediate: true } { immediate: true }
) )
const menus = ref([ const menus = ref([
{ label: 'Copy', tip: 'Ctrl+C', on: () => emit('copy-node') }, { label: t('FlowCanvas.copy'), tip: 'Ctrl+C', on: () => emit('copy-node') },
{ {
label: 'Delete', label: t('FlowCanvas.delete'),
tip: 'Del', tip: 'Del',
on: () => { on: () => {
emit('delete-node', props.node.id) emit('delete-node', props.node.id)
@@ -135,23 +138,8 @@
disabled: !!props.config?.disableDelete disabled: !!props.config?.disableDelete
}, },
{ isDivide: true }, { isDivide: true },
// {
// label: 'Bring to font',
// tip: '',
// on: () => {
// emit('bring-to-font')
// }
// },
// {
// label: 'Send to back',
// tip: '',
// on: () => {
// emit('send-to-back')
// }
// },
// { isDivide: true },
{ {
label: 'Flip horizontal', label: t('FlowCanvas.flipHorizontal'),
tip: '', tip: '',
on: () => { on: () => {
data.imageProcessTasks.forEach((item) => { data.imageProcessTasks.forEach((item) => {
@@ -167,7 +155,7 @@
} }
}, },
{ {
label: 'Flip vertical', label: t('FlowCanvas.flipVertical'),
tip: '', tip: '',
on: () => { on: () => {
data.imageProcessTasks.forEach((item) => { data.imageProcessTasks.forEach((item) => {
@@ -184,7 +172,7 @@
} }
]) ])
const onPreview = (item: any) => { const onPreview = (item: any) => {
if(data.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL){ if(data.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL && tier.value != 0){
openThreeModelPreview({ openThreeModelPreview({
glbPath:item?.glbPath, glbPath:item?.glbPath,
glbInfoObj:item?.glbInfoObj, glbInfoObj:item?.glbInfoObj,
@@ -238,6 +226,15 @@
} }
depthCanvasWorkbench(workbenchData) depthCanvasWorkbench(workbenchData)
depthCanvasClose() depthCanvasClose()
//更新VersionNode的sketchIDAndUrl中指定key的URL值
updateVersionSketchUrlApi({
sketchId: stateManager.sketchId.value,
newUrl: options.url,
}).then(()=>{
//更新VersionNode的versionImgUpdataList中指定key的URL值
let initialNode = stateManager.getInitialNode()
initialNode.data.versionImgUpdataList.push(options.url)
})
}, },
onClose:depthCanvasClose onClose:depthCanvasClose
} }

View File

@@ -24,6 +24,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, onMounted, nextTick, watch } from 'vue' import { reactive, ref, onMounted, nextTick, watch } from 'vue'
import myTextTools from '@/components/Canvas/FlowCanvas/components/tools/my-textTools.vue' import myTextTools from '@/components/Canvas/FlowCanvas/components/tools/my-textTools.vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const props = defineProps({ const props = defineProps({
active: { active: {
type: Boolean, type: Boolean,
@@ -36,7 +38,7 @@
}) })
const emit = defineEmits(['update-data', 'delete-node']) const emit = defineEmits(['update-data', 'delete-node'])
const data = reactive({ const data = reactive({
text: props.data?.text || '点击编辑文本', text: props.data?.text || t('FlowCanvas.clickEditText'),
textStyle:{ textStyle:{
'--font-size':props.data?.textStyle?.['--font-size'] || '16px', '--font-size':props.data?.textStyle?.['--font-size'] || '16px',
'--font-color':props.data?.textStyle?.['--font-color'] || '#000', '--font-color':props.data?.textStyle?.['--font-color'] || '#000',

View File

@@ -38,7 +38,7 @@
import { reactive, ref, markRaw, onMounted } from 'vue' import { reactive, ref, markRaw, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const emit = defineEmits(['update:textStyle']) const emit = defineEmits(['update:textStyle'])
const { locale } = useI18n() const { locale, t } = useI18n()
const fontFamilyList = ref({ const fontFamilyList = ref({
ENGLISH: [ ENGLISH: [
{ value:'Medium',label:'Medium' }, { value:'Medium',label:'Medium' },

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="my-textarea"> <div class="my-textarea">
<textarea <textarea
:placeholder="placeholder" :placeholder="placeholder || $t('FlowCanvas.promptDefaultPlaceholder')"
:value="modelValue" :value="modelValue"
@input="onInput" @input="onInput"
@change="onChange" @change="onChange"
@@ -16,12 +16,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, markRaw, onMounted } from 'vue' import { computed, ref, markRaw, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const placeholder = t('FlowCanvas.promptDefaultPlaceholder')
const emit = defineEmits(['update:modelValue', 'input', 'change']) const emit = defineEmits(['update:modelValue', 'input', 'change'])
const props = defineProps({ const props = defineProps({
modelValue: { type: String }, modelValue: { type: String },
placeholder: { placeholder: {
type: String, type: String,
default: 'Enter the scene you want to describe...' default: ''
} }
}) })
const onInput = (e) => { const onInput = (e) => {

View File

@@ -28,21 +28,13 @@ const {} = toRefs(data);
</script> </script>
<template> <template>
<div class="modalDetail"> <div class="modalDetail">
<div class="title"> <div class="title">{{ $t('threeModel.propertiesInformation') }}</div>
Properties Information
</div>
<div class="detail"> <div class="detail">
<div class="name"> <div class="name">
<div class="title fs18"> <div class="title fs18">{{ $t('threeModel._3DAsset') }}</div>
Sofa <div class="fs14 c66">{{ $t('threeModel.fileFormat') }}</div>
</div>
<div class="fs14 c66">
Model Name
</div>
</div>
<div class="fs14 c18">
Transform
</div> </div>
<div class="fs14 c18">{{ $t('threeModel.transform') }}</div>
<div class="flex"> <div class="flex">
<div> <div>
<div class="fs14 c18">X</div> <div class="fs14 c18">X</div>
@@ -57,20 +49,18 @@ const {} = toRefs(data);
<div class="fs12 c66">{{ config?.glbInfoObj?.centroid?.[2].toFixed(2) || 0 }}</div> <div class="fs12 c66">{{ config?.glbInfoObj?.centroid?.[2].toFixed(2) || 0 }}</div>
</div> </div>
</div> </div>
<div class="fs14 c18"> <div class="fs14 c18">{{ $t('threeModel.dimensions') }}</div>
Dimensions
</div>
<div class="flex"> <div class="flex">
<div> <div>
<div class="fs14 c18">Height</div> <div class="fs14 c18">{{ $t('threeModel.height') }}</div>
<div class="fs12 c66">{{ config?.glbInfoObj?.size?.[0].toFixed(2) || 0 }}</div> <div class="fs12 c66">{{ config?.glbInfoObj?.size?.[0].toFixed(2) || 0 }}</div>
</div> </div>
<div> <div>
<div class="fs14 c18">Width</div> <div class="fs14 c18">{{ $t('threeModel.width') }}</div>
<div class="fs12 c66">{{ config?.glbInfoObj?.size?.[1].toFixed(2) || 0 }}</div> <div class="fs12 c66">{{ config?.glbInfoObj?.size?.[1].toFixed(2) || 0 }}</div>
</div> </div>
<div> <div>
<div class="fs14 c18">Depth</div> <div class="fs14 c18">{{ $t('threeModel.depth') }}</div>
<div class="fs12 c66">{{ config?.glbInfoObj?.size?.[2].toFixed(2) || 0 }}</div> <div class="fs12 c66">{{ config?.glbInfoObj?.size?.[2].toFixed(2) || 0 }}</div>
</div> </div>
</div> </div>
@@ -100,7 +90,7 @@ const {} = toRefs(data);
} }
> .captureView , > .captureView ,
> .download{ > .download{
margin-left: 4.2rem; transform: translateX(calc(13rem / 2));
line-height: 3rem; line-height: 3rem;
width: 20rem; width: 20rem;
border-radius: 1.5rem; border-radius: 1.5rem;

View File

@@ -41,6 +41,9 @@ const captureView = ()=>{
onMounted(()=>{ onMounted(()=>{
}) })
onUnmounted(()=>{ onUnmounted(()=>{
console.log('onUnmounted')
threeModel.disposeModel()
threeModel = null
}) })
defineExpose({open,captureView}) defineExpose({open,captureView})
</script> </script>

View File

@@ -24,7 +24,7 @@ export class ThreeManager {
camera: THREE.PerspectiveCamera;//相机对象 camera: THREE.PerspectiveCamera;//相机对象
renderer: THREE.WebGLRenderer;//渲染器对象 renderer: THREE.WebGLRenderer;//渲染器对象
controls: OrbitControls;//轨道控制器对象 controls: OrbitControls;//轨道控制器对象
animationId: number | null = null;
pointLight: THREE.AmbientLight;//环境光对象 pointLight: THREE.AmbientLight;//环境光对象
studioLights: any;//工作室光对象数组 studioLights: any;//工作室光对象数组
@@ -316,14 +316,13 @@ export class ThreeManager {
} }
operation(){ operation(){
let this_ = this
const animate = () => { const animate = () => {
requestAnimationFrame(animate); this.animationId = requestAnimationFrame(animate);
this.controls.update(); this.controls?.update();
this.updateStudioLighting(); this.updateStudioLighting();
this.updateImagePipeline(); this.updateImagePipeline();
this.renderer.render(this.scene, this.camera); this.renderer?.render(this.scene, this.camera);
} };
animate(); animate();
} }
@@ -343,7 +342,91 @@ export class ThreeManager {
} }
}); });
} }
disposeModel() {
console.log('开始销毁 ThreeManager...');
// 1. 停止动画
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
// 2. 销毁控制器
if (this.controls) {
this.controls.dispose();
this.controls = null;
}
// 3. 销毁模型
if (this.currentModel) {
this.dispose(this.currentModel);
this.scene?.remove(this.currentModel);
this.currentModel = null;
}
// 4. 销毁灯光
if (this.studioLights?.length) {
this.studioLights.forEach(item => {
if (item.light) {
this.scene?.remove(item.light);
item.light.dispose?.();
}
});
this.studioLights = [];
}
// 5. 清理环境光
if (this.pointLight) {
this.scene?.remove(this.pointLight);
this.pointLight = null;
}
// 6. 清理场景
if (this.scene) {
this.scene.traverse((obj) => {
if (obj.isMesh) {
const mesh = obj as THREE.Mesh;
mesh.geometry?.dispose();
if (mesh.material) {
if (Array.isArray(mesh.material)) {
mesh.material.forEach(m => m.dispose());
} else {
mesh.material.dispose();
}
}
}
});
while (this.scene.children.length) {
this.scene.remove(this.scene.children[0]);
}
this.scene = null;
}
// 7. 销毁渲染器
if (this.renderer) {
this.renderer.dispose();
if (this.renderer.domElement?.parentNode) {
this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
}
this.renderer = null;
}
// 8. 清空 DOM 容器
if (this.threeDom) {
this.threeDom.innerHTML = '';
this.threeDom = null;
}
// 9. 清空其他引用
this.camera = null;
this.v1 = null;
this.camDir = null;
this.camForward = null;
this.camToTarget = null;
this.modelInfo = null;
console.log('ThreeManager 已销毁');
}
exportAsImage(){ exportAsImage(){
return this.renderer.domElement.toDataURL('image/png'); return this.renderer.domElement.toDataURL('image/png');

View File

@@ -8,8 +8,8 @@
</div> </div>
<div class="control" v-else> <div class="control" v-else>
<div class="icon"><svg-icon name="upload" size="17" size-unit="px" /></div> <div class="icon"><svg-icon name="upload" size="17" size-unit="px" /></div>
<div class="txt">{{ tip }}</div> <div class="txt">{{ tip || $t('FlowCanvas.uploadFiles') }}</div>
<div class="btn" @click="onSelectFile">Select File</div> <div class="btn" @click="onSelectFile">{{ $t('FlowCanvas.selectFile') }}</div>
</div> </div>
</div> </div>
</template> </template>
@@ -20,7 +20,7 @@
const emit = defineEmits(['update:modelValue', 'change']) const emit = defineEmits(['update:modelValue', 'change'])
const props = defineProps({ const props = defineProps({
modelValue: { type: [File, Object, String, null] }, modelValue: { type: [File, Object, String, null] },
tip: { type: String, default: 'Upload your files' } tip: { type: String, default: '' }
}) })
const data = reactive({ const data = reactive({
file: null file: null

View File

@@ -289,6 +289,7 @@
const timestamp = Date.now() const timestamp = Date.now()
nodeManager.createResultNode({ nodeManager.createResultNode({
data: { data: {
versionImgUpdataList:[props.config.url],
disableDelete: true, disableDelete: true,
isHeader: false, isHeader: false,
data: { data: {
@@ -307,6 +308,9 @@
}) })
} }
}) })
const putFlowJson = async () => {
await stateManager.exportFlow(0,true)
}
onBeforeUnmount(() => { onBeforeUnmount(() => {
stateManager.dispose() stateManager.dispose()
eventManager.dispose() eventManager.dispose()
@@ -315,7 +319,8 @@
toolManager.dispose() toolManager.dispose()
}) })
defineExpose({ defineExpose({
getFlowJson getFlowJson,
putFlowJson
}) })
</script> </script>
<style lang="less"> <style lang="less">

View File

@@ -10,6 +10,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { getSketchFlowCanvas } from '@/api/flow-canvas' import { getSketchFlowCanvas } from '@/api/flow-canvas'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import myEvent from '@/utils/myEvent'
const dialogVisible = ref(false) const dialogVisible = ref(false)
const config = ref({}) as any const config = ref({}) as any
@@ -17,22 +18,27 @@
const {t:$t} = useI18n() const {t:$t} = useI18n()
const open = async (options) => { const open = async (options) => {
let json = [] let json = []
let isGetJson = false
await new Promise((resolve) => { await new Promise((resolve) => {
getSketchFlowCanvas({ id: options.imgId },true).then((res:any) => { getSketchFlowCanvas({ id: options.imgId },true).then((res:any) => {
if (res) { if (res) {
json = JSON.parse(res) json = JSON.parse(res)
} }
isGetJson = true
resolve(true) resolve(true)
}).catch(() => { }).catch(() => {
resolve(true) resolve(false)
}) })
}) })
if(!isGetJson)return
config.value = options || {} config.value = options || {}
config.value.json = json config.value.json = json
dialogVisible.value = true dialogVisible.value = true
} }
const close = async () => { const close = async () => {
await flowCanvasRef.value?.putFlowJson()
dialogVisible.value = false dialogVisible.value = false
myEvent.emit('closeFlowCanvas')
} }
defineExpose({ defineExpose({
open, open,

View File

@@ -108,6 +108,7 @@ export class EventManager {
const list = [ const list = [
{ key: "ctrl-c", handler: () => this.handleCopy(event, activeNodeID) }, { key: "ctrl-c", handler: () => this.handleCopy(event, activeNodeID) },
{ key: "delete", handler: () => this.handleDelete(event, activeNodeID) }, { key: "delete", handler: () => this.handleDelete(event, activeNodeID) },
{ key: "Backspace", handler: () => this.handleDelete(event, activeNodeID) },
{ key: "ctrl-z", handler: () => this.stateManager.undoState() }, { key: "ctrl-z", handler: () => this.stateManager.undoState() },
{ key: "ctrl-shift-z", handler: () => this.stateManager.redoState() }, { key: "ctrl-shift-z", handler: () => this.stateManager.redoState() },
] ]

View File

@@ -1,8 +1,12 @@
import { getTaskidResult } from '@/api/flow-canvas' import { getTaskidResult } from '@/api/flow-canvas'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { updateVersionSketchUrlApi } from '@/api/flow-canvas'
import i18n from '@/lang'
// interface NodeOptions { // interface NodeOptions {
// } // }
const t = i18n.global.t
export class GenerateManager { export class GenerateManager {
stateManager: any stateManager: any
taskIds: string[] = [] taskIds: string[] = []
@@ -54,10 +58,20 @@ export class GenerateManager {
nodeDataItem.url = item.url nodeDataItem.url = item.url
nodeDataItem.createTime = item.createTime nodeDataItem.createTime = item.createTime
nodeDataItem.status = item.status nodeDataItem.status = item.status
//更新VersionNode的sketchIDAndUrl中指定key的URL值
updateVersionSketchUrlApi({
sketchId: this.stateManager.sketchId.value,
newUrl: item.url,
}).then(()=>{
//更新VersionNode的versionImgUpdataList中指定key的URL值
let initialNode = this.stateManager.getInitialNode()
initialNode.data.versionImgUpdataList.push(item.url)
})
if(item.glbPath){ if(item.glbPath){
nodeDataItem.glbPath = item.glbPath nodeDataItem.glbPath = item.glbPath
nodeDataItem.glbInfoObj = item.glbInfoObj nodeDataItem.glbInfoObj = item.glbInfoObj
} }
ElMessage.success(t('FlowCanvas.generateSuccess'))
} }
} }
}) })

View File

@@ -14,6 +14,7 @@ interface NodeData {
disableCopy?: boolean// 是否禁用复制 disableCopy?: boolean// 是否禁用复制
createIndexPosition?: number// 创建索引位置 createIndexPosition?: number// 创建索引位置
isActive?: boolean// 是否激活 isActive?: boolean// 是否激活
versionImgUpdataList?: Array<any>// 版本节点
} }
interface NodeOptions { interface NodeOptions {
id?: string id?: string
@@ -98,7 +99,6 @@ export class NodeManager {
} }
/** 创建结果节点 */ /** 创建结果节点 */
createResultNode(options?: NodeOptions) { createResultNode(options?: NodeOptions) {
console.log(options)
const options_ = { const options_ = {
...(options ? options : {}), ...(options ? options : {}),
component: NODE_COMPONENT.RESULT_IMAGE, component: NODE_COMPONENT.RESULT_IMAGE,

View File

@@ -4,37 +4,42 @@ import { ElMessageBox } from 'element-plus'
import i18n from '@/lang' import i18n from '@/lang'
import { putSketchFlowCanvas } from '@/api/flow-canvas' import { putSketchFlowCanvas } from '@/api/flow-canvas'
import myEvent from '@/utils/myEvent' import myEvent from '@/utils/myEvent'
import { updateVersionSketchUrlApi } from '@/api/flow-canvas'
const t = i18n.global.t const t = i18n.global.t
//推送到对话框的助手 //推送到对话框的助手
const chatAssistant = { const chatAssistant = {
[NODE_DATATYPE.TO_REAL_STYLE]:{ [NODE_DATATYPE.TO_REAL_STYLE]:{
content: t('flowCanvas.toRealStyleDesignAssistant'), content: t('FlowCanvas.toRealStyleDesignAssistant'),
nodeType:NODE_DATATYPE.TO_REAL_STYLE, nodeType:NODE_DATATYPE.TO_REAL_STYLE,
}, },
[NODE_DATATYPE.CANVAS_MODE]:{ [NODE_DATATYPE.CANVAS_MODE]:{
content: t('flowCanvas.surfaceEditCanvasDesignAssistant'), content: t('FlowCanvas.surfaceEditCanvasDesignAssistant'),
nodeType:NODE_DATATYPE.CANVAS_MODE, nodeType:NODE_DATATYPE.CANVAS_MODE,
}, },
[NODE_DATATYPE.Fast_MODE]:{ [NODE_DATATYPE.Fast_MODE]:{
content: t('flowCanvas.surfaceEditAIDesignAssistant'), content: t('FlowCanvas.surfaceEditAIDesignAssistant'),
nodeType:NODE_DATATYPE.Fast_MODE, nodeType:NODE_DATATYPE.Fast_MODE,
}, },
[NODE_DATATYPE.COLOR_PALETTE]:{ [NODE_DATATYPE.COLOR_PALETTE]:{
content: t('flowCanvas.colorPaletteDesignAssistant'), content: t('FlowCanvas.colorPaletteDesignAssistant'),
nodeType:NODE_DATATYPE.COLOR_PALETTE, nodeType:NODE_DATATYPE.COLOR_PALETTE,
}, },
[NODE_DATATYPE.SCENE_COMPOSITION]:{ [NODE_DATATYPE.SCENE_COMPOSITION]:{
content: t('flowCanvas.threeModelDesignAssistant'), content: t('FlowCanvas.sceneCompositionDesignAssistant'),
nodeType:NODE_DATATYPE.SCENE_COMPOSITION, nodeType:NODE_DATATYPE.SCENE_COMPOSITION,
}, },
[NODE_DATATYPE.TO_3D_MODEL]:{ [NODE_DATATYPE.TO_3D_MODEL]:{
content: t('flowCanvas.threeModelDesignAssistant'), content: t('FlowCanvas.threeModelDesignAssistant'),
nodeType:NODE_DATATYPE.TO_3D_MODEL,
},
[NODE_DATATYPE.TO_REAL_VARIANTS]:{
content: t('FlowCanvas.toRealVariantsDesignAssistant'),
nodeType:NODE_DATATYPE.TO_3D_MODEL, nodeType:NODE_DATATYPE.TO_3D_MODEL,
}, },
[NODE_DATATYPE.TO_3VIEW]:{ [NODE_DATATYPE.TO_3VIEW]:{
content: t('flowCanvas.threeModelDesignAssistant'), content: t('FlowCanvas.to3DViewDesignAssistant'),
nodeType:NODE_DATATYPE.TO_3VIEW, nodeType:NODE_DATATYPE.TO_3VIEW,
}, },
} }
@@ -157,6 +162,8 @@ export class StateManager {
this.nodes.value = this.nodes.value.filter((node: NodesItem) => node.id !== id) this.nodes.value = this.nodes.value.filter((node: NodesItem) => node.id !== id)
} }
/** 获取初始节点 */
getInitialNode() { return this.nodes.value.find((node: NodesItem) => node?.data?.versionImgUpdataList) }
/** 获取节点 */ /** 获取节点 */
getNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.id === id) } getNodeById(id: string) { return this.nodes.value.find((node: NodesItem) => node.id === id) }
/** 获取下级节点 */ /** 获取下级节点 */
@@ -167,7 +174,7 @@ export class StateManager {
getSuperiorNodeImage(superiorID: string) { getSuperiorNodeImage(superiorID: string) {
const superiorNode = this.getNodeById(superiorID) const superiorNode = this.getNodeById(superiorID)
if(!superiorNode){ if(!superiorNode){
// ElMessage.error(t('flowCanvas.cannotFindSuperiorImage')) // ElMessage.error(t('FlowCanvas.cannotFindSuperiorImage'))
return null return null
} }
const superiorNodeUrl = superiorNode.data.data.imageProcessTasks.filter((item)=>{ const superiorNodeUrl = superiorNode.data.data.imageProcessTasks.filter((item)=>{
@@ -179,15 +186,22 @@ export class StateManager {
async deleteSubordinateAllNodes(id: string,{ isElMessageBox } = { isElMessageBox: false }) { async deleteSubordinateAllNodes(id: string,{ isElMessageBox } = { isElMessageBox: false }) {
const node = this.getNodeById(id) const node = this.getNodeById(id)
if (!node) return console.warn(`没有找到指定id:${id}`) if (!node) return console.warn(`没有找到指定id:${id}`)
if (node.data.disableDelete) return ElMessage.error(t('flowCanvas.initialNodeProhibited')) if (node.data.disableDelete) return ElMessage.error(t('FlowCanvas.initialNodeProhibited'))
const result = [node] const result = [node]
const deleteVersionImgUpdataList = []
node.data.data?.imageProcessTasks?.forEach((item) =>{
deleteVersionImgUpdataList.push(item.url)
})
const findChildren = (parentId: string) => { const findChildren = (parentId: string) => {
const children = this.nodes.value.filter(item => item.data.superiorID === parentId) const children = this.nodes.value.filter(item => item.data.superiorID === parentId)
children.forEach(child => { children.forEach(child => {
if(child.data.type !== NODE_DATATYPE.RESULT_IMAGE){ if(child.data.type !== NODE_DATATYPE.RESULT_IMAGE){
result.push(child) result.push(child)
console.log(child)
child.data.data?.imageProcessTasks?.forEach((item) =>{
deleteVersionImgUpdataList.push(item.url)
})
} }
findChildren(child.id) findChildren(child.id)
}) })
@@ -197,11 +211,11 @@ export class StateManager {
if (isElMessageBox) { if (isElMessageBox) {
deletePromise = await new Promise<void>((resolve, reject) => { deletePromise = await new Promise<void>((resolve, reject) => {
ElMessageBox.confirm( ElMessageBox.confirm(
result.length > 1 ? t('flowCanvas.deleteSubordinateCard') : t('flowCanvas.deleteCardConfirm'), result.length > 1 ? t('FlowCanvas.deleteSubordinateCard') : t('FlowCanvas.deleteCardConfirm'),
'', '',
{ {
confirmButtonText: t('flowCanvas.confirm'), confirmButtonText: t('FlowCanvas.confirm'),
cancelButtonText: t('flowCanvas.cancel'), cancelButtonText: t('FlowCanvas.cancel'),
} }
).then(() => {resolve(true) ).then(() => {resolve(true)
}).catch(() => { }).catch(() => {
@@ -211,6 +225,22 @@ export class StateManager {
} }
if(!deletePromise) return console.log('删除操作被取消') if(!deletePromise) return console.log('删除操作被取消')
// 删除如果是最后一张图需要更新版本图url
let initialNode = this.getInitialNode()
let updataListIndex
let isUpdataEndUrl = false
deleteVersionImgUpdataList?.forEach((item) => {
updataListIndex = initialNode.data.versionImgUpdataList.indexOf(item)
if(updataListIndex == (initialNode.data.versionImgUpdataList.length - 1))isUpdataEndUrl = true
if(initialNode.data.versionImgUpdataList.length > 1)initialNode.data.versionImgUpdataList = initialNode.data.versionImgUpdataList.filter((v) => v !== item)
})
if(isUpdataEndUrl){
updateVersionSketchUrlApi({
sketchId: this.sketchId.value,
newUrl: initialNode.data.versionImgUpdataList[initialNode.data.versionImgUpdataList.length - 1],
})
}
if(node.data.data?.imageProcessTasks?.length > 1){ if(node.data.data?.imageProcessTasks?.length > 1){
node.data.data.imageProcessTasks = node.data.data.imageProcessTasks.filter((item) => item.taskId !== node.data.data.selectTaskId) node.data.data.imageProcessTasks = node.data.data.imageProcessTasks.filter((item) => item.taskId !== node.data.data.selectTaskId)
return return
@@ -259,7 +289,7 @@ export class StateManager {
this.historyIndex.value = this.historyList.value.length - 1 this.historyIndex.value = this.historyList.value.length - 1
} }
/** 画布数据存储 */ /** 画布数据存储 */
async exportFlow (time:number = 0){ async exportFlow (time:number = 0,loading:boolean = false){
if(!this.sketchId.value)return if(!this.sketchId.value)return
clearTimeout(this.saveCanvasTime) clearTimeout(this.saveCanvasTime)
await new Promise((resolve) => { await new Promise((resolve) => {
@@ -268,7 +298,7 @@ export class StateManager {
id: this.sketchId.value, id: this.sketchId.value,
canvasData: JSON.stringify(this.nodes.value) }).then(() => { canvasData: JSON.stringify(this.nodes.value) }).then(() => {
resolve(true) resolve(true)
}).catch(() => { },loading).catch(() => {
resolve(true) resolve(true)
}) })
},time) },time)

View File

@@ -28,6 +28,9 @@ export const NODE_DATATYPE = {
SURFACE_EDIT: 'surface-edit', SURFACE_EDIT: 'surface-edit',
CANVAS_MODE: 'canvas-mode', CANVAS_MODE: 'canvas-mode',
Fast_MODE: 'fast-mode', Fast_MODE: 'fast-mode',
SURFACE_EDIT_: 'surface-edit_',
CANVAS_MODE_: 'canvas-mode_',
Fast_MODE_: 'fast-mode_',
SCENE_COMPOSITION: 'scene-composition', SCENE_COMPOSITION: 'scene-composition',
COLOR_PALETTE: 'color-palette', COLOR_PALETTE: 'color-palette',
TO_3D_MODEL: 'to-3d-model', TO_3D_MODEL: 'to-3d-model',
@@ -44,6 +47,9 @@ export const NODE_DATATIER = {
SURFACE_EDIT: 1, SURFACE_EDIT: 1,
CANVAS_MODE: 1, CANVAS_MODE: 1,
Fast_MODE: 1, Fast_MODE: 1,
SURFACE_EDIT_: 2,
CANVAS_MODE_: 2,
Fast_MODE_: 2,
SCENE_COMPOSITION: 2, SCENE_COMPOSITION: 2,
COLOR_PALETTE: 2, COLOR_PALETTE: 2,
TO_3D_MODEL: 2, TO_3D_MODEL: 2,

View File

@@ -41,13 +41,13 @@ export default {
retrievePassword: 'Retrieve password' retrievePassword: 'Retrieve password'
}, },
Nuic: { Nuic: {
hiName: 'Hi, {name}. This is Fiphant.', hiName: `Hi {name}, I'm Fiphant.`,
nuic1Title: `Help him discover the <b>"YOU"</b> in your space.`, nuic1Title: `Lets reveal the creative paths waiting for you.`,
nuic1Tip: `Let's set up your profile. A few quick details will help Fiphant understand<br />your needs and find exactly what you're looking for.`, nuic1Tip: `Let's set up your profile. A few quick details will help Fiphant understand<br />your needs and find exactly what you're looking for.`,
letsGo: 'Lets go, Fiphant!', letsGo: 'Lets go, Fiphant!',
skip: 'Skip', skip: 'Skip',
next: 'Next', next: 'Next',
nuic2Title: `What <b>vibe</b> do you usually go for?`, nuic2Title: `Which <b>vibe</b> do you usually go for?`,
loadMore: 'Load more', loadMore: 'Load more',
nuic3Title: `<b>Where</b> are you based? What do you <b>do</b>?`, nuic3Title: `<b>Where</b> are you based? What do you <b>do</b>?`,
basedIn: 'Based in', basedIn: 'Based in',
@@ -193,7 +193,7 @@ export default {
Confirm: 'Confirm', Confirm: 'Confirm',
export: 'Export' export: 'Export'
}, },
flowCanvas: { FlowCanvas: {
deleteCardConfirm: 'Are you sure you want to delete this function card?', deleteCardConfirm: 'Are you sure you want to delete this function card?',
confirm: 'Confirm', confirm: 'Confirm',
cancel: 'Cancel', cancel: 'Cancel',
@@ -215,6 +215,8 @@ export default {
"🔄 I'll turn your render into a 3D model you can rotate and look at from any angle. I'd recommend paying close attention to the corner joints, leg proportions, and seat depth — these are the spots that are easy to miss in a sketch but tend to cause the most trouble during prototyping. Better to catch them now while it's easy to fix.", "🔄 I'll turn your render into a 3D model you can rotate and look at from any angle. I'd recommend paying close attention to the corner joints, leg proportions, and seat depth — these are the spots that are easy to miss in a sketch but tend to cause the most trouble during prototyping. Better to catch them now while it's easy to fix.",
to3DViewDesignAssistant: to3DViewDesignAssistant:
"📐 We're at the final step! I'll export your 3D model as front, side, and top view!", "📐 We're at the final step! I'll export your 3D model as front, side, and top view!",
toRealVariantsDesignAssistant:
'🎨 Ive converted your 3D model into multiple product view cards. Pick the one that feels most natural, sketch directly on it, adjust the structure, or add details—then use this feature to generate a brand-new product render in one click!',
toRealVariantsShortcut1Label: 'Change the...', toRealVariantsShortcut1Label: 'Change the...',
toRealVariantsShortcut1Value: 'Change the sofa backrest shape.', toRealVariantsShortcut1Value: 'Change the sofa backrest shape.',
toRealVariantsShortcut2Label: 'The chair legs...', toRealVariantsShortcut2Label: 'The chair legs...',
@@ -226,6 +228,77 @@ export default {
toRealVariantsShortcut5Label: 'Modified the outer...', toRealVariantsShortcut5Label: 'Modified the outer...',
toRealVariantsShortcut5Value: 'Modified the outer contour of the chair back.', toRealVariantsShortcut5Value: 'Modified the outer contour of the chair back.',
toRealVariantsPlaceholder: 'Enter the furniture details you modified...', toRealVariantsPlaceholder: 'Enter the furniture details you modified...',
export: 'Export',
edit: 'Edit',
generate: 'Generate',
copy: 'Copy',
delete: 'Delete',
flipHorizontal: 'Flip horizontal',
flipVertical: 'Flip vertical',
clickEditText: 'Click to edit text',
// 卡片工具标题
selectCardsTitle: 'Advanced Tools',
toRealStyleTitle: 'To Real Style',
toRealVariantsTitle: 'To Real Variants',
surfaceEditTitle: 'Surface Edit',
surfaceEditCanvasTitle: 'Surface Edit (Canvas)',
sceneCompositionTitle: 'Scene Composition',
colorPaletteTitle: 'Color Palette',
to3DModelTitle: 'To 3D Model',
to3ViewTitle: 'To 3D View',
// 卡片工具
print: 'Print',
settings: 'Settings',
angle: 'Angle',
scale: 'Scale',
gapX: 'Gap X',
gapY: 'Gap Y',
offset: 'Offset',
size: 'Size',
prompt: 'Prompt',
promptDefaultPlaceholder: 'Enter the scene you want to describe...',
mode: 'Mode',
advancedMode: 'Advanced',
normalMode: 'Normal',
chooseColor: 'Choose Color',
output: 'Output',
chooseStyle: 'Choose Style',
colorful: 'Colorful',
minimalist: 'Minimalist',
modernist: 'Modernist',
bauhaus: 'Bauhaus',
mintage: 'Mintage',
industrial: 'Industrial',
futuristic: 'Futuristic',
elegant: 'Elegant',
organic: 'Organic',
calm: 'Calm',
abstract: 'Abstract',
kitschCore: 'Kitsch-core',
sophisticated: 'Sophisticated',
maximalism: 'Maximalism',
clean: 'Clean',
brightColors: 'Bright Colors',
luxurious: 'Luxurious',
boldColors: 'Bold Colors',
brutalism: 'Brutalism',
image: 'Image',
_3DModel: '3D Model',
toRealStyleShortcut1Label: 'Change the...',
toRealStyleShortcut1Value: 'Change the style to a realistic design. ',
toRealStyleShortcut2Label: 'Bright Colors...',
toRealStyleShortcut2Value: 'Bright colors with modern patterns, change the style to a realistic furniture design. ',
toRealStyleShortcut3Label: 'Make the...',
toRealStyleShortcut3Value: "RMake the structure more refined and balanced, change the style to a realistic furniture style. ",
toRealStyleShortcut4Label: 'Imagine...',
toRealStyleShortcut4Value: 'Imagine this furniture with detailed fabric textures, change the style to a realistic design. ',
toRealStyleShortcut5Label: 'Wood Materials with...',
toRealStyleShortcut5Value: 'Wood materials with natural oak texture and soft fabric, change the style to a realistic furniture design.',
// 上传文件组件
selectFile: 'Select File',
uploadFiles: 'Upload your files',
//生成成功
generateSuccess: 'New results have been generated.',
}, },
assistant: { assistant: {
inputPlaceholder: 'Ask anything' inputPlaceholder: 'Ask anything'
@@ -235,6 +308,14 @@ export default {
loading: 'Loading', loading: 'Loading',
download: 'Download', download: 'Download',
captureView: 'Capture View', captureView: 'Capture View',
propertiesInformation: 'Properties Information',
_3DAsset: '3D Asset',
fileFormat: 'File Format:glb.',
transform: 'Transform',
dimensions: 'Dimensions',
width: 'Width',
height: 'Height',
depth: 'Depth',
}, },
DepthCanvas: { DepthCanvas: {
layer: 'Layer', layer: 'Layer',

View File

@@ -42,8 +42,8 @@ export default {
retrievePassword: '找回密码' retrievePassword: '找回密码'
}, },
Nuic: { Nuic: {
hiName: '你好{name}。这是 Fiphant。', hiName: '你好{name},我是 Fiphant。',
nuic1Title: `帮助他发现您空间中的 <b>“YOU”</b>`, nuic1Title: `让我们为您揭晓那些等待着您的创意之路`,
nuic1Tip: `让我们设置您的个人资料。几个快速的细节将帮助 Fiphant 理解您的需求并找到您正在寻找的内容。`, nuic1Tip: `让我们设置您的个人资料。几个快速的细节将帮助 Fiphant 理解您的需求并找到您正在寻找的内容。`,
letsGo: '让我们开始Fiphant', letsGo: '让我们开始Fiphant',
skip: '跳过', skip: '跳过',
@@ -189,7 +189,7 @@ export default {
delete: '删除', delete: '删除',
edit: '编辑' edit: '编辑'
}, },
flowCanvas: { FlowCanvas: {
deleteCardConfirm: '确定要删除该功能卡片吗?', deleteCardConfirm: '确定要删除该功能卡片吗?',
confirm: '确认', confirm: '确认',
cancel: '取消', cancel: '取消',
@@ -210,6 +210,8 @@ export default {
'🔄 我把你的效果图变成可以转着看的立体模型,你可以从各个角度检查一下结构。我建议重点看看转角、腿脚比例和座面厚度——这几个地方在草图里不容易发现问题,但打样的时候最容易出偏差,现在发现比较好改。', '🔄 我把你的效果图变成可以转着看的立体模型,你可以从各个角度检查一下结构。我建议重点看看转角、腿脚比例和座面厚度——这几个地方在草图里不容易发现问题,但打样的时候最容易出偏差,现在发现比较好改。',
to3DViewDesignAssistant: to3DViewDesignAssistant:
'📐 我们到最后一步了!我来帮你把 3D 模型导出为前视图、侧视图和俯视图!', '📐 我们到最后一步了!我来帮你把 3D 模型导出为前视图、侧视图和俯视图!',
toRealVariantsDesignAssistant:
'🎨 我已经帮你把 3D 模型转换成多个视角的产品卡片,你可以选一张最顺手的,在上面直接涂鸦、改结构或加细节,再使用该功能一键生成全新的产品渲染图!',
toRealVariantsShortcut1Label: '修改...', toRealVariantsShortcut1Label: '修改...',
toRealVariantsShortcut1Value: '修改了沙发靠背的形状.', toRealVariantsShortcut1Value: '修改了沙发靠背的形状.',
toRealVariantsShortcut2Label: '椅子腿...', toRealVariantsShortcut2Label: '椅子腿...',
@@ -221,6 +223,77 @@ export default {
toRealVariantsShortcut5Label: '修改...', toRealVariantsShortcut5Label: '修改...',
toRealVariantsShortcut5Value: '修改了椅背的外轮廓.', toRealVariantsShortcut5Value: '修改了椅背的外轮廓.',
toRealVariantsPlaceholder: '请输入修改后的家具详情...', toRealVariantsPlaceholder: '请输入修改后的家具详情...',
export: '导出',
edit: '编辑',
generate: '生成',
copy: '复制',
delete: '删除',
flipHorizontal: '水平翻转',
flipVertical: '垂直翻转',
clickEditText: '点击编辑文本',
// 卡片工具标题
selectCardsTitle: '高级工具',
toRealStyleTitle: '真实风格',
toRealVariantsTitle: '真实风格变体',
surfaceEditTitle: '表面编辑',
surfaceEditCanvasTitle: '表面编辑Canvas',
sceneCompositionTitle: '场景构图',
colorPaletteTitle: '颜色调色板',
to3DModelTitle: '3D模型',
to3ViewTitle: '3视图',
// 卡片工具
print: '印花',
settings: '设置',
angle: '角度',
scale: '缩放',
gapX: '水平间距',
gapY: '垂直间距',
offset: '偏移量',
size: '大小',
prompt: '提示',
promptDefaultPlaceholder: '请输入你想要描述的场景…',
mode: '模式',
advancedMode: '高级',
normalMode: '普通',
chooseColor: '选择颜色',
output: '输出',
chooseStyle: '选择风格',
colorful: '多彩',
minimalist: '极简',
modernist: '现代',
bauhaus: '包豪斯',
mintage: '复古',
industrial: '工业',
futuristic: '未来',
elegant: '优雅',
organic: '有机',
calm: '平静',
abstract: '抽象',
kitschCore: '媚俗核心',
sophisticated: '精致',
maximalism: '极繁',
clean: '干净',
brightColors: '明亮色彩',
luxurious: '豪华',
boldColors: '大胆色彩',
brutalism: '粗野主义',
image: '图片',
_3DModel: '3D模型',
toRealStyleShortcut1Label: '将图像风格...',
toRealStyleShortcut1Value: '将图像风格转换为真实画风。.',
toRealStyleShortcut2Label: '采用明亮色彩...',
toRealStyleShortcut2Value: '采用明亮色彩与现代图案,将家具图像风格转换为真实画风。',
toRealStyleShortcut3Label: '使结构更加...',
toRealStyleShortcut3Value: "使结构更加精致与平衡,将家具图像风格转换为真实画风。",
toRealStyleShortcut4Label: '想象该家具具有...',
toRealStyleShortcut4Value: '想象该家具具有细腻的织物纹理,将家具图像风格转换为真实画风。',
toRealStyleShortcut5Label: '使用天然橡木纹理...',
toRealStyleShortcut5Value: '使用天然橡木纹理的木质材料与柔软织物,将家具图像风格转换为真实画风。',
// 上传文件组件
selectFile: '选择文件',
uploadFiles: '上传文件',
//生成成功
generateSuccess: '新的结果已生成',
}, },
assistant: { assistant: {
inputPlaceholder: '请输入' inputPlaceholder: '请输入'
@@ -230,6 +303,14 @@ export default {
loading: '加载中', loading: '加载中',
download: '下载', download: '下载',
captureView: '捕获视图', captureView: '捕获视图',
propertiesInformation: '属性信息',
_3DAsset: '3D资产',
fileFormat: '文件格式glb',
transform: '变换',
dimensions: '尺寸',
width: '宽度',
height: '高度',
depth: '深度',
}, },
DepthCanvas: { DepthCanvas: {
layer: '图层', layer: '图层',

View File

@@ -16,6 +16,7 @@ type InitialProjectData = {
useReport:boolean useReport:boolean
needSuggestion:boolean needSuggestion:boolean
quoteList: Array<string> quoteList: Array<string>
parameterTags?: Array<{ kind: string; label: string }>
tempImages: any[] tempImages: any[]
} }
export const useAgentStore = defineStore('agent', () => { export const useAgentStore = defineStore('agent', () => {

View File

@@ -37,8 +37,12 @@
import type { AgentParamsType } from '@/api/agent' import type { AgentParamsType } from '@/api/agent'
import { useUserInfoStore, useProjectStore, useAgentStore } from '@/stores' import { useUserInfoStore, useProjectStore, useAgentStore } from '@/stores'
import MyEvent from '@/utils/myEvent' import MyEvent from '@/utils/myEvent'
import { areaList } from '@/utils/area'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { createStyleOptions, createTypeOptions } from '../../components/input/options'
import type { OptionItem, ParameterTag } from '../../components/input/types'
const { t } = useI18n() const { t } = useI18n()
const route = useRoute() const route = useRoute()
@@ -83,6 +87,59 @@
}) })
const sketchList = ref([]) const sketchList = ref([])
const typeOptions = createTypeOptions()
const areaOptions = areaList
const styleOptions = createStyleOptions()
const getTranslatedOptionLabel = (options: OptionItem[], value?: string) => {
if (!value) return ''
const option = options.find((item) => item.value === value)
return option ? t(option.label) : value
}
const getOptionLabel = (options: OptionItem[], value?: string) => {
if (!value) return ''
return options.find((item) => item.value === value)?.label || value
}
const getProjectParameterTags = (project: any): ParameterTag[] => {
const type = project?.type || ''
const region = project?.region || project?.area || ''
const style = project?.style || ''
const tags: ParameterTag[] = []
if (type) {
tags.push({
kind: 'type',
label: getTranslatedOptionLabel(typeOptions, type)
})
}
if (region) {
tags.push({
kind: 'area',
label: getTranslatedOptionLabel(areaOptions, region)
})
}
if (style) {
tags.push({
kind: 'style',
label: getOptionLabel(styleOptions, style)
})
}
return tags
}
const applyProjectParameterTags = (messages: any[], project: any) => {
const parameterTags = getProjectParameterTags(project)
if (parameterTags.length === 0) return
const firstUserMessage = messages.find((item) => item.isUser || item.role === 'user')
if (firstUserMessage) {
firstUserMessage.parameterTags = parameterTags
}
}
watch( watch(
sketchList, sketchList,
(newVal) => { (newVal) => {
@@ -143,7 +200,8 @@
images: initialData.images, images: initialData.images,
useReport: initialData.useReport, useReport: initialData.useReport,
tempImages: initialData.tempImages, tempImages: initialData.tempImages,
quoteList: initialData.quoteList quoteList: initialData.quoteList,
parameterTags: initialData.parameterTags || []
}) })
// 更新 configParams // 更新 configParams
@@ -181,13 +239,14 @@
tempImages: any[] tempImages: any[]
useReport: boolean useReport: boolean
quoteList: Array<string> quoteList: Array<string>
parameterTags?: Array<{ kind: string; label: string }>
}, },
skipUserMessage = false skipUserMessage = false
) => { ) => {
isPaused.value = false isPaused.value = false
isGenerating.value = true isGenerating.value = true
params.message = message.text params.message = message.text
if (message.hasOwnProperty('useReport')) { if (Object.prototype.hasOwnProperty.call(message, 'useReport')) {
params.useReport = message.useReport params.useReport = message.useReport
} }
@@ -199,7 +258,8 @@
id: messageList.value.length + 1, id: messageList.value.length + 1,
text: message.text, text: message.text,
isUser: true, isUser: true,
imageUrls: message.tempImages.concat(message.quoteList) imageUrls: message.tempImages.concat(message.quoteList),
parameterTags: message.parameterTags || []
}) })
} }
@@ -589,7 +649,7 @@
thinkingText: combinedThinkingText, thinkingText: combinedThinkingText,
text: combinedContent, text: combinedContent,
image_url: combinedImageUrl, image_url: combinedImageUrl,
webAddress: !!webAddress ? JSON.parse(webAddress) : null, webAddress: webAddress ? JSON.parse(webAddress) : null,
isUser: false, isUser: false,
id: result.length + 1, id: result.length + 1,
sessionId: sessionId sessionId: sessionId
@@ -628,7 +688,7 @@
const setChatInfo = (info) => { const setChatInfo = (info) => {
const initialData = agentStore.getInitialProjectData const initialData = agentStore.getInitialProjectData
if (isGenerating.value || initialData) return if (isGenerating.value || initialData) return
console.log('---',info)
const data = info.conversation const data = info.conversation
let project = info.project let project = info.project
if (info.id) { if (info.id) {
@@ -641,7 +701,7 @@
if (project) { if (project) {
params.configParams.type = project.type || '' params.configParams.type = project.type || ''
params.configParams.region = project.area || '' params.configParams.region = project.region || project.area || ''
params.configParams.style = project.style || '' params.configParams.style = project.style || ''
params.configParams.temperature = project.temperature params.configParams.temperature = project.temperature
params.projectID = project.id params.projectID = project.id
@@ -668,6 +728,7 @@
item.text += `<slot slot-name="sketch"></slot>` item.text += `<slot slot-name="sketch"></slot>`
} }
}) })
applyProjectParameterTags(ancestorsList, project)
// console.log('ancestorslist', ancestorsList) // console.log('ancestorslist', ancestorsList)
messageList.value = [...ancestorsList] messageList.value = [...ancestorsList]
params.versionID = current?.id params.versionID = current?.id
@@ -688,7 +749,8 @@
} }
defineExpose({ defineExpose({
setChatInfo setChatInfo,
isGenerating
}) })
</script> </script>

View File

@@ -52,20 +52,34 @@
</div> </div>
</div> </div>
</div> --> </div> -->
<div class="message-txt markdown-body flex flex-col"> <div class="message-txt markdown-body">
<span
v-if="parameterTags.length > 0"
class="message-parameter-tags"
>
<span
v-for="tag in parameterTags"
:key="`${tag.kind}-${tag.label}`"
class="message-parameter-tag"
>
<img :src="getParameterTagIcon(tag.kind)" class="parameter-tag-icon" />
<span>{{ tag.label }}</span>
</span>
</span>
<VueMarkdown <VueMarkdown
class="message-markdown"
:custom-attrs="customAttrs" :custom-attrs="customAttrs"
:markdown="content.text" :markdown="content.text"
:rehype-plugins="[rehypeRaw]" :rehype-plugins="[rehypeRaw]"
> >
<template v-slot:s-card="{ children: children, ...attrs }"> <template v-slot:s-card="{ children: children, ...attrs }">
<Card title="Trend Report" @click.native="handleClickReport" /> <Card title="Trend Report" @click="handleClickReport" />
</template> </template>
<template v-slot:s-url="{ children: children }"> <template v-slot:s-url="{ children: children }">
<Url :list="content.webAddress" @click.native="handleClickUrls" /> <Url :list="content.webAddress" @click="handleClickUrls" />
</template> </template>
<template v-slot:s-sketch="{ children: children }"> <template v-slot:s-sketch="{ children: children }">
<Sketch @click.native="handleClickSketch" /> <Sketch @click="handleClickSketch" />
</template> </template>
</VueMarkdown> </VueMarkdown>
<div <div
@@ -116,6 +130,9 @@
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import userThumb from '@/assets/images/user-thumb.jpg' import userThumb from '@/assets/images/user-thumb.jpg'
import agentThumb from '@/assets/images/agent-thumb.png' import agentThumb from '@/assets/images/agent-thumb.png'
import TypeIcon from '@/assets/icons/TypeIcon.svg'
import RegionIcon from '@/assets/icons/RegionIcon.svg'
import StyleIcon from '@/assets/icons/StyleIcon.svg'
import Card from './ReportCard.vue' import Card from './ReportCard.vue'
import Url from './UrlCard.vue' import Url from './UrlCard.vue'
import Sketch from './SketchCard.vue' import Sketch from './SketchCard.vue'
@@ -126,6 +143,7 @@
import rehypeRaw from 'rehype-raw' import rehypeRaw from 'rehype-raw'
import MyEvent from '@/utils/myEvent' import MyEvent from '@/utils/myEvent'
import { useUserInfoStore, useProjectStore } from '@/stores' import { useUserInfoStore, useProjectStore } from '@/stores'
import { ElMessage } from 'element-plus'
const userStore = useUserInfoStore() const userStore = useUserInfoStore()
const projectStore = useProjectStore() const projectStore = useProjectStore()
@@ -136,8 +154,14 @@
return locale.value === 'CHINESE_SIMPLIFIED' return locale.value === 'CHINESE_SIMPLIFIED'
}) })
type ParameterTagKind = 'type' | 'area' | 'style'
type ParameterTag = {
kind: ParameterTagKind
label: string
}
const props = defineProps<{ const props = defineProps<{
content: Object content: any
isLast: Boolean isLast: Boolean
}>() }>()
@@ -174,6 +198,16 @@
return list return list
}) })
const parameterTags = computed<ParameterTag[]>(() => {
return Array.isArray(props.content?.parameterTags) ? props.content.parameterTags : []
})
const getParameterTagIcon = (kind: ParameterTagKind) => {
if (kind === 'type') return TypeIcon
if (kind === 'area') return RegionIcon
return StyleIcon
}
const customAttrs: CustomAttrs = { const customAttrs: CustomAttrs = {
img: { img: {
style: 'max-width: 100%;' style: 'max-width: 100%;'
@@ -348,6 +382,39 @@
width: fit-content; width: fit-content;
max-width: 82%; max-width: 82%;
} }
.message-parameter-tags {
display: inline;
}
.message-parameter-tag {
display: inline-flex;
align-items: center;
vertical-align: middle;
height: 3rem;
max-width: 24rem;
padding: 0 1rem;
margin: 0 0.8rem 0.4rem 0;
border: 0.1rem solid #0000001a;
border-radius: 2.2rem;
column-gap: 0.8rem;
font-family: 'Medium';
font-weight: 500;
font-size: 1.2rem;
color: #0d0d0d;
background-color: #fff;
box-sizing: border-box;
.parameter-tag-icon {
width: 1.2rem;
height: 1.2rem;
flex-shrink: 0;
object-fit: contain;
}
span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.web-address { .web-address {
width: fit-content; width: fit-content;
min-width: 22.5rem; min-width: 22.5rem;
@@ -443,6 +510,18 @@
<style lang="less"> <style lang="less">
.message-txt { .message-txt {
user-select: text; user-select: text;
.message-markdown,
> div:not(.web-address) {
display: inline;
}
.message-markdown > p:first-child,
> div:not(.web-address) > p:first-child {
display: inline;
margin-top: 0;
}
ul { ul {
list-style-position: inside; list-style-position: inside;
} }

View File

@@ -6,7 +6,7 @@
> >
<template v-if="type === 'sketch'"> <template v-if="type === 'sketch'">
<div <div
class="sketch-item" class="sketch-item flex flex-center"
v-for="(item, index) in combineSketchList" v-for="(item, index) in combineSketchList"
:key="'sketch-item-' + index" :key="'sketch-item-' + index"
> >
@@ -329,12 +329,12 @@
.sketch-item { .sketch-item {
position: relative; position: relative;
width: calc((100% - 1.2rem * 3) / 4); width: calc((100% - 1.2rem * 3) / 4);
//设置比例
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
border-radius: 1.6rem; border-radius: 1.6rem;
background-color: #fff;
img { img {
width: 100%; width: 100%;
height: 100%; // height: 100%;
border-radius: 1.6rem; border-radius: 1.6rem;
} }
.loading-wrapper { .loading-wrapper {

View File

@@ -1,7 +1,9 @@
<template> <template>
<div class="agent-wrapper flex space-between"> <div class="agent-wrapper flex space-between">
<div class="openVersionTree"> <div class="openVersionTree">
<div class="btn" @click="versionTreeData.drawer = true">Version Tree</div> <div class="btn" :class="{ 'is-disabled': isAgentGenerating }" @click="handleOpenVersionTree">
Version Tree
</div>
</div> </div>
<div class="content-wrapper"> <div class="content-wrapper">
<KeepAlive :max="10"> <KeepAlive :max="10">
@@ -55,6 +57,7 @@
const VersionTreeIndexRef = ref() const VersionTreeIndexRef = ref()
const agentRef = ref() const agentRef = ref()
const sketchList = ref([]) const sketchList = ref([])
const isAgentGenerating = computed(() => Boolean(agentRef.value?.isGenerating))
const updateSketchList = (newVal) => { const updateSketchList = (newVal) => {
sketchList.value = newVal sketchList.value = newVal
// VersionTreeIndexRef.value.getVersionTree() // VersionTreeIndexRef.value.getVersionTree()
@@ -63,7 +66,7 @@
const handleDeleteSketch = (id) => { const handleDeleteSketch = (id) => {
sketchList.value = sketchList.value sketchList.value = sketchList.value
.map((sketchItem) => { .map((sketchItem) => {
if (sketchItem.hasOwnProperty(id)) { if (Object.prototype.hasOwnProperty.call(sketchItem, id)) {
delete sketchItem[id] delete sketchItem[id]
} }
return sketchItem return sketchItem
@@ -79,6 +82,11 @@
drawer: false drawer: false
}) })
const handleOpenVersionTree = () => {
if (isAgentGenerating.value) return
versionTreeData.value.drawer = true
}
const handleRestore = () => { const handleRestore = () => {
// agentRef.value?.inputRef?.addReportTag('Restore') // agentRef.value?.inputRef?.addReportTag('Restore')
clearNodeChat({ projectId: projectStore.state.id, id: projectStore.state.nodeId }).then( clearNodeChat({ projectId: projectStore.state.id, id: projectStore.state.nodeId }).then(
@@ -154,6 +162,7 @@
MyEvent.add('openUrls', handleOpenUrls) MyEvent.add('openUrls', handleOpenUrls)
MyEvent.add('openSketch', handleOpenSketch) MyEvent.add('openSketch', handleOpenSketch)
MyEvent.add('renameConversation', handleRenameConversation) MyEvent.add('renameConversation', handleRenameConversation)
MyEvent.add('closeFlowCanvas', handleGetProjectInfoAndHistory)
projectStore.clearProject() projectStore.clearProject()
if (proJectId.value) { if (proJectId.value) {
handleGetProjectInfoAndHistory() handleGetProjectInfoAndHistory()
@@ -162,8 +171,9 @@
onUnmounted(() => { onUnmounted(() => {
MyEvent.remove('openReport', handleOpenReport) MyEvent.remove('openReport', handleOpenReport)
MyEvent.remove('openUrls', handleOpenUrls) MyEvent.remove('openUrls', handleOpenUrls)
MyEvent.remove('OpenSketch', handleOpenSketch) MyEvent.remove('openSketch', handleOpenSketch)
MyEvent.remove('renameConversation', handleRenameConversation) MyEvent.remove('renameConversation', handleRenameConversation)
MyEvent.remove('closeFlowCanvas', handleGetProjectInfoAndHistory)
}) })
</script> </script>
@@ -201,6 +211,10 @@
border-radius: 2.5rem; border-radius: 2.5rem;
z-index: -1; z-index: -1;
} }
&.is-disabled {
cursor: not-allowed;
opacity: 0.6;
}
} }
} }

View File

@@ -1,172 +0,0 @@
# Input 组件文档
## 组件概述
`Input.vue` 是一个输入框组件,用于家具设计项目的创建和AI对话场景。主要提供文本输入、图片上传、选项选择和项目创建等功能。
## 组件位置
`src/views/home/components/Input.vue`
## Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `isAgentMode` | `boolean` | `false` | 是否为Agent模式(简化界面) |
| `generating` | `boolean` | `false` | 是否正在生成内容 |
## Events
| 事件名 | 参数 | 说明 |
|--------|------|------|
| `send` | `{ text: string, images: string[], tempImages: Array }` | 发送消息/创建项目 |
| `pause` | - | 暂停生成 |
## 组件模式
### 普通模式 (isAgentMode = false)
完整的输入界面,包含以下功能:
- 图片上传附件按钮
- 类型选择下拉框 (Sofa/Desk/Chair)
- 区域选择下拉框
- 风格选择弹窗 (15种风格)
- 设置弹窗 (3个滑块参数)
- 创建项目按钮
### Agent模式 (isAgentMode = true)
简化界面,仅包含:
- 文本输入区域
- 发送/暂停按钮
## 功能详解
### 1. 图片上传
- 支持通过附件按钮或拖拽上传图片
- 图片预览区域显示已上传图片
- 可删除单个图片
- 图片通过 `uploadImage` API 上传
### 2. 富文本编辑
使用 `contenteditable` div 实现:
- 支持粘贴纯文本
- 支持回车发送/创建
- 支持退格键删除标签
### 3. 热点报告标签
点击底部按钮添加特殊标签:
- 标签包含图标和文本
- 可通过关闭按钮删除
- 插入到编辑器光标位置
### 4. 风格选择弹窗
15种家具风格可选:
```
Venetian Modern, Coastal, Maximalism, Memphis, Verdant,
Century Chrome, Modern Revival, Transitional, Tuscan 2000's,
Kitsch-core, Bauhaus, Constructivism, Nordic Noir, Dopamine, Squiggle
```
### 5. 设置弹窗
3个滑块参数配置 (默认值50%):
- 对应翻译路径: `Input.settingOptions.first/second/third`
## 关键方法
| 方法名 | 作用域 | 说明 |
|--------|--------|------|
| `triggerFileUpload` | 公开 | 触发文件选择 |
| `handleFileChange` | 私有 | 处理文件选择变化 |
| `removeImage` | 私有 | 删除指定索引的图片 |
| `handleEditorInput` | 私有 | 处理编辑器输入 |
| `handleEditorPaste` | 私有 | 处理粘贴事件 |
| `handleKeyDown` | 私有 | 处理键盘事件 |
| `handleSendAgent` | 私有 | Agent模式发送 |
| `handleCreateProject` | 私有 | 创建项目 |
| `addReportTag` | 公开 | 添加热点报告标签 |
| `toogltReportTag` | 私有 | 切换热点报告标签 |
| `selectStyle` | 私有 | 选择风格 |
| `confirmStyle` | 私有 | 确认风格选择 |
| `confirmSetting` | 私有 | 确认设置 |
## 公开API (defineExpose)
```typescript
defineExpose({
addReportTag: (text?: string) => void
})
```
## 数据结构
### 上传图片格式
```typescript
interface UploadedImage {
url: string // 预览URL (base64)
name: string // 文件名
path: string // 服务器路径
}
```
### 发送Payload格式
```typescript
interface SendPayload {
text: string // 输入文本
images: string[] // 图片服务器路径
tempImages: UploadedImage[] // 预览图片数据
}
```
### 创建项目参数
```typescript
interface ProjectParams {
type: string // 家具类型
area: string // 区域
style: string // 风格
temperature: number // 温度参数 (固定0.7)
}
```
## 依赖项
- **Vue API**: `computed`, `ref`, `watch`, `nextTick`, `onMounted`
- **Store**: `useAgentStore`, `useProjectStore`
- **Utils**: `areaList` (区域列表), `getStyleImage` (风格图片)
- **API**: `createProject`, `uploadImage`
- **UI库**: Element Plus (`el-select`, `el-popover`, `el-slider`)
- **国际化**: `vue-i18n`
## 样式说明
- 使用 `<style lang="less">` scoped 样式
- 全局样式部分使用非 scoped (`.fida-` 前缀类名)
- 支持 hover 动画效果
- Agent 模式下样式有所简化
## 注意事项
1. 编辑器使用 `contenteditable`,需手动处理输入事件
2. 图片上传后使用 FileReader 读取本地预览
3. 标签使用 DOM 操作动态插入
4. 项目创建后会路由跳转至 `/home/agent/{projectId}`
5. 组件会自动聚焦编辑器
## 国际化键名参考
- `Input.placeholder` - 输入框占位符
- `Input.typePlaceholder` - 类型选择占位符
- `Input.areaPlaceholder` - 区域选择占位符
- `Input.stylePlaceholder` - 风格选择占位符
- `Input.chooseStyle` - 选择风格标题
- `Input.confirm` - 确认按钮
- `Input.styleTitle` - 设置弹窗标题
- `Input.createProject` - 创建项目按钮
- `Input.trendingReport` - 热点报告按钮文本
- `Input.types.sofa/desk/chair` - 家具类型选项
- `Input.settingOptions.first/second/third` - 设置选项

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,99 @@
<template>
<div class="agent-operate flex flex-center">
<el-popover placement="top" trigger="click" popper-class="agent-plus-popover">
<template #reference>
<SvgIcon name="plus" color="#0D0D0D" size="16" />
</template>
<template #default>
<div class="agent-modal flex flex-col">
<div class="file flex align-center" @click="emit('upload')">
<img src="@/assets/icons/attach.svg" class="file-icon" />
<span>Upload files</span>
</div>
<div class="gap"></div>
<div class="report flex align-center" @click="emit('toggle-report')">
<SvgIcon color="#5A5A5A" name="light" size="11" />
<span>Trending report</span>
</div>
</div>
</template>
</el-popover>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(e: 'upload'): void
(e: 'toggle-report'): void
}>()
</script>
<style lang="less" scoped>
.agent-operate {
width: 3.2rem;
height: 3.2rem;
cursor: pointer;
border-radius: 50%;
&:hover {
background-color: #f0f0f0;
}
}
</style>
<style lang="less">
.agent-plus-popover {
display: flex;
justify-content: flex-start;
background: transparent !important;
box-shadow: none !important;
border: none !important;
padding: 0 !important;
margin-bottom: -1rem;
width: fit-content !important;
min-width: initial !important;
.el-popper__arrow:before {
display: none;
}
.agent-modal {
row-gap: 1.2rem;
font-family: 'Medium';
font-weight: 500;
font-size: 1.3rem;
color: #222;
background-color: #fff;
border: 1px solid #d9d9d9;
box-shadow: 0px 6.53px 32.63px 0px #0000000d;
border-radius: 1rem;
padding: 1.2rem 1.4rem;
transform: translateX(calc(50% - 1.6rem));
.c-svg {
width: initial;
width: 1rem;
height: 1.3rem;
}
.file,
.report {
line-height: 1.8rem;
column-gap: 1rem;
cursor: pointer;
}
.file {
.file-icon {
width: 1.1rem;
height: 1.3rem;
}
}
.gap {
height: 0.05rem;
background-color: #d4d4d4;
}
}
}
</style>

View File

@@ -0,0 +1,234 @@
<template>
<div class="editor-wrapper" :class="{ agent: isAgentMode }">
<div v-if="showPlaceholder" class="editor-placeholder">
{{ placeholder }}
</div>
<div
ref="editorRef"
class="editor"
contenteditable="true"
@input="emit('input')"
@paste="emit('paste', $event)"
@keydown="emit('keydown', $event)"
></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
withDefaults(
defineProps<{
showPlaceholder?: boolean
placeholder: string
isAgentMode?: boolean
}>(),
{
showPlaceholder: false,
isAgentMode: false
}
)
const emit = defineEmits<{
(e: 'ready', element: HTMLDivElement | null): void
(e: 'input'): void
(e: 'paste', event: ClipboardEvent): void
(e: 'keydown', event: KeyboardEvent): void
}>()
const editorRef = ref<HTMLDivElement | null>(null)
const focus = () => {
editorRef.value?.focus()
}
onMounted(() => {
emit('ready', editorRef.value)
})
defineExpose({
editorRef,
focus
})
</script>
<style lang="less" scoped>
.editor-wrapper {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
min-height: 0;
.editor-placeholder {
position: absolute;
z-index: 0;
pointer-events: none;
top: 0;
left: 0;
padding: 0 1.4rem 1.4rem;
font-size: 2rem;
font-family: 'InterRegular';
font-weight: 400;
color: #999;
white-space: pre-wrap;
word-wrap: break-word;
width: calc(100% - 2.8rem);
box-sizing: border-box;
}
.editor {
width: 100%;
flex: 1;
border: none;
outline: none;
padding: 0 1.4rem 1.4rem;
font-size: 1.8rem;
font-family: 'InterRegular';
font-weight: 400;
color: #000000;
overflow-y: auto;
overflow-x: hidden;
line-height: 2.8rem;
white-space: pre-wrap;
word-wrap: break-word;
&::-webkit-scrollbar {
width: 0;
display: none;
}
&::-webkit-scrollbar-thumb,
&::-webkit-scrollbar-track {
display: none;
}
&:empty::before {
content: attr(placeholder);
color: #999;
pointer-events: none;
}
}
&.agent {
.editor {
font-family: 'Regular';
font-weight: 400;
font-size: 1.4rem;
min-height: 5rem;
max-height: initial;
padding: 0;
height: 100%;
line-height: 1.4rem;
}
.editor-placeholder {
font-family: 'Regular';
font-size: 1.4rem;
padding: 0;
line-height: 1.4rem;
}
}
}
</style>
<style lang="less">
.assist-input-wrapper .editor .editor-tag {
width: 15.6rem;
height: 3.1rem;
display: inline-flex;
border: 1px solid #0000001a;
font-weight: 500;
font-size: 1.3rem;
column-gap: 0;
margin: 0 0.5rem;
vertical-align: middle;
border-radius: 2.2rem;
&.restore {
width: auto;
max-width: 100%;
display: inline-flex;
border: none;
border-radius: 0.4rem;
background-color: rgba(0, 0, 0, 0.05);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
align-items: center;
justify-content: space-between;
padding: 0 0.9rem 0 0.7rem;
box-sizing: border-box;
}
&.option-tag {
width: auto;
max-width: 24rem;
height: 3.4rem;
padding: 0 1rem;
box-sizing: border-box;
align-items: center;
column-gap: 0.8rem;
color: #0d0d0d;
&:hover {
cursor: pointer;
}
.option-tag-icon {
width: 1.2rem;
height: 1.2rem;
flex-shrink: 0;
object-fit: contain;
}
.option-tag-text {
margin: 0;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.option-tag-close {
cursor: pointer;
}
}
span {
margin: 0 0.7rem 0 1.2rem;
&.restore-text {
flex: 1;
min-width: 0;
max-width: 52rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.light-icon {
width: 0.9rem;
height: 1.2rem;
flex-shrink: 0;
}
.close-icon {
width: 0.9rem;
height: 0.9rem;
cursor: pointer;
flex-shrink: 0;
&.restore {
width: 0.5rem;
height: 0.5rem;
}
}
.restore-icon {
width: 1.2rem;
height: 1.2rem;
}
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<div v-if="previewItems.length > 0" class="image-preview-list flex wrap">
<div
v-for="(image, index) in previewItems"
:key="`${index}-${getPreviewUrl(image)}`"
class="image-preview-item"
>
<img
:src="getPreviewUrl(image)"
class="preview-image"
@click="emit('preview', getPreviewUrl(image))"
/>
<div class="image-remove-btn" @click="emit('remove', index, image)">
<SvgIcon name="delete" size="16" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { PreviewImage, UploadedImage } from './types'
const props = defineProps<{
uploadedImages: UploadedImage[]
quoteList: string[]
}>()
const emit = defineEmits<{
(e: 'preview', url: string): void
(e: 'remove', index: number, image: PreviewImage): void
}>()
const previewItems = computed<PreviewImage[]>(() => [
...props.uploadedImages,
...props.quoteList
])
const getPreviewUrl = (image: PreviewImage) => {
return typeof image === 'string' ? image : image.url
}
</script>
<style lang="less" scoped>
.image-preview-list {
padding: 0 1.4rem 1rem;
column-gap: 1rem;
max-height: 15rem;
overflow-y: auto;
flex-shrink: 0;
.image-preview-item {
position: relative;
width: 8.6rem;
height: 8.6rem;
border-radius: 1.5rem;
overflow: hidden;
flex-shrink: 0;
border: 0.1rem solid #cdcdcd;
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0.8rem;
cursor: pointer;
}
.image-remove-btn {
position: absolute;
top: 0.6rem;
right: 0.6rem;
width: 1.6rem;
height: 1.6rem;
color: #fff;
border-radius: 50%;
align-items: center;
justify-content: center;
font-size: 1.2rem;
cursor: pointer;
display: none;
}
&:hover .image-remove-btn {
display: flex;
}
}
}
</style>

View File

@@ -0,0 +1,282 @@
<template>
<div class="operate input-toolbar flex align-center space-between" :class="{ agent: isAgentMode }">
<div class="left flex align-center">
<AgentOperatePopover
v-if="isAgentMode"
@upload="triggerFileUpload"
@toggle-report="emit('toggle-report')"
/>
<div
v-if="!isAgentMode"
class="attach flex flex-center"
@click="triggerFileUpload"
>
<img src="@/assets/icons/attach.svg" />
</div>
<input
ref="fileInputRef"
type="file"
accept="image/*"
style="display: none"
@change="emit('file-change', $event)"
/>
<el-select
v-if="!isAgentMode"
v-model="typeModel"
:placeholder="typePlaceholder"
:disabled="parametersDisabled"
>
<el-option
v-for="item in typeOptions"
class="input-option"
:key="item.value"
:label="translate(item.label)"
:value="item.value"
/>
</el-select>
<el-select
v-if="!isAgentMode"
v-model="areaModel"
:placeholder="areaPlaceholder"
:disabled="parametersDisabled"
>
<el-option
v-for="item in areaOptions"
class="input-option"
:key="item.value"
:label="translate(item.label)"
:value="item.value"
/>
</el-select>
<StyleSelect
v-if="!isAgentMode"
v-model="styleModel"
:type-value="typeValue"
:options="styleOptions"
:placeholder="stylePlaceholder"
:title="styleTitle"
:confirm-text="confirmText"
:disabled="parametersDisabled"
/>
<!-- <SettingPopover
v-model:options="settingOptionsModel"
:title="settingTitle"
:translate="translate"
/> -->
</div>
<div class="right">
<div
v-if="!isAgentMode"
class="create-btn flex flex-center"
@click="emit('create')"
>
<img src="@/assets/images/shining.png" class="shining-icon" alt="" />
<span class="create-btn-text">{{ createText }}</span>
</div>
<div v-else class="sender-btn flex flex-center" @click="emit('send')">
<img
v-show="!generating"
src="@/assets/images/sender.png"
alt=""
class="sender-icon"
/>
<div v-show="generating" class="sender-pause" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import AgentOperatePopover from './AgentOperatePopover.vue'
import SettingPopover from './SettingPopover.vue'
import StyleSelect from './StyleSelect.vue'
import type { OptionItem, SettingOption } from './types'
const props = withDefaults(
defineProps<{
isAgentMode?: boolean
generating?: boolean
typeValue: string
areaValue: string
styleValue: string
typeOptions: OptionItem[]
areaOptions: OptionItem[]
styleOptions: OptionItem[]
settingOptions: SettingOption[]
typePlaceholder: string
areaPlaceholder: string
stylePlaceholder: string
styleTitle: string
settingTitle: string
confirmText: string
createText: string
parametersDisabled?: boolean
translate: (key: string) => string
}>(),
{
isAgentMode: false,
generating: false,
parametersDisabled: false
}
)
const emit = defineEmits<{
(e: 'update:typeValue', value: string): void
(e: 'update:areaValue', value: string): void
(e: 'update:styleValue', value: string): void
(e: 'update:settingOptions', value: SettingOption[]): void
(e: 'file-change', event: Event): void
(e: 'toggle-report'): void
(e: 'create'): void
(e: 'send'): void
}>()
const fileInputRef = ref<HTMLInputElement | null>(null)
const typeModel = computed({
get: () => props.typeValue,
set: (value: string) => emit('update:typeValue', value)
})
const areaModel = computed({
get: () => props.areaValue,
set: (value: string) => emit('update:areaValue', value)
})
const styleModel = computed({
get: () => props.styleValue,
set: (value: string) => emit('update:styleValue', value)
})
const settingOptionsModel = computed({
get: () => props.settingOptions,
set: (value: SettingOption[]) => emit('update:settingOptions', value)
})
const triggerFileUpload = () => {
fileInputRef.value?.click()
}
</script>
<style lang="less" scoped>
.input-toolbar {
flex-shrink: 0;
margin-top: auto;
padding: 0 1.7rem 1.7rem;
.left {
column-gap: 2rem;
}
.attach {
width: 4rem;
height: 4rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
cursor: pointer;
img {
width: 1.65rem;
height: 1.86rem;
}
}
.el-select {
width: 13.9rem;
height: 4rem;
:deep(.el-select__wrapper) {
border-radius: 0.8rem;
height: 100%;
box-shadow: none;
border: 0.1rem solid rgba(0, 0, 0, 0.1);
font-weight: 500;
font-size: 1.4rem;
min-height: initial;
.el-select__placeholder {
color: #000;
}
.el-select__icon {
color: #000;
}
&.is-disabled {
.el-select__placeholder,
.el-select__selected-item {
color: #c9c9c9;
}
}
}
}
.create-btn {
background-color: #ff7a51;
height: 4rem;
width: 13rem;
color: #fff;
border-radius: 4.2rem;
font-family: 'SemiBold';
font-weight: 600;
font-size: 1.3rem;
cursor: pointer;
column-gap: 0.3rem;
.shining-icon {
width: 1.4rem;
height: 1.4rem;
}
}
&.agent {
padding: 1.2rem 0 0;
margin: 0;
.right {
display: flex;
align-items: center;
.sender-btn {
width: 3.2rem;
height: 3.2rem;
cursor: pointer;
background-color: #ff7a51;
border-radius: 50%;
&:hover {
background-color: #f8693d;
}
.sender-icon {
width: 1.3rem;
height: 1.3rem;
}
.sender-pause {
width: 1rem;
height: 1rem;
background-color: #fff;
}
}
}
}
}
</style>
<style lang="less">
.input-option {
padding-left: 2rem !important;
color: #0d0d0d;
font-weight: 510;
font-size: 1.3rem;
height: 3rem;
line-height: 3rem;
&.el-select-dropdown__item.is-hovering {
background-color: rgba(13, 13, 13, 0.05);
}
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div
class="report-btn flex space-between align-center outer"
:class="{ 'is-cn': isCn }"
@click="emit('click')"
>
<SvgIcon class="light-icon" color="#FFDB56" name="light" size="16" />
<span>{{ label }}</span>
</div>
</template>
<script setup lang="ts">
defineProps<{
isCn: boolean
label: string
}>()
const emit = defineEmits<{
(e: 'click'): void
}>()
</script>
<style lang="less" scoped>
.report-btn {
position: absolute;
bottom: -7.4rem;
height: 4.4rem;
border-radius: 2.2rem;
width: 19.7rem;
padding: 0 2rem;
font-size: 1.8rem;
background-color: #fff;
border: 1.1px solid #f6f4ef1a;
cursor: pointer;
&.outer.is-cn {
justify-content: center;
column-gap: 3rem;
}
.c-svg {
width: 1.5rem;
height: 2rem;
}
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<el-popover
v-model:visible="settingPopupVisible"
placement="top"
:width="342"
:show-arrow="false"
trigger="click"
popper-class="fida-setting-popover"
>
<template #reference>
<img src="@/assets/images/setting.png" class="setting-icon" />
</template>
<div class="fida-setting-popover-content flex flex-col">
<div class="fida-setting-popover-header">
{{ title }}
</div>
<div class="fida-setting-slider-list">
<div
v-for="(item, index) in options"
:key="item.label"
class="fida-setting-slider-item"
>
<div class="fida-slider-label">{{ translate(item.label) }}</div>
<div class="fida-slider-row flex align-center">
<el-slider
class="setting-popover-slider"
:model-value="item.value"
:show-tooltip="false"
@update:model-value="updateOptionValue(index, $event)"
/>
<span class="fida-slider-value">{{ item.value }}%</span>
</div>
</div>
</div>
</div>
</el-popover>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { SettingOption } from './types'
const props = defineProps<{
options: SettingOption[]
title: string
translate: (key: string) => string
}>()
const emit = defineEmits<{
(e: 'update:options', value: SettingOption[]): void
}>()
const settingPopupVisible = ref(false)
const updateOptionValue = (index: number, value: number | number[]) => {
const nextValue = Array.isArray(value) ? value[0] : value
const nextOptions = props.options.map((item, itemIndex) =>
itemIndex === index ? { ...item, value: nextValue } : item
)
emit('update:options', nextOptions)
}
</script>
<style lang="less" scoped>
.setting-icon {
width: 2.4rem;
height: 2.4rem;
cursor: pointer;
}
</style>
<style lang="less">
.fida-setting-popover {
padding: 0 !important;
border-radius: 0.6rem !important;
background-color: #fff !important;
border: none !important;
width: 25.6rem;
height: 23.9rem;
box-shadow: 0px 11px 20px 0px #0000001a;
}
.fida-setting-popover-header {
font-weight: 400;
font-size: 1.4rem;
color: #000;
margin-bottom: 2rem !important;
}
.fida-setting-popover-content {
padding: 1.6rem 1.4rem 2.2rem !important;
}
.fida-setting-slider-list {
display: flex;
flex-direction: column;
row-gap: 1rem;
}
.fida-setting-slider-item {
.fida-slider-label {
font-weight: 400;
font-size: 1.2rem;
color: #000;
margin-bottom: 1rem;
}
.fida-slider-row {
column-gap: 2.6rem;
.el-slider {
flex: 1;
}
.fida-slider-value {
font-weight: 400;
font-size: 1.4rem;
color: #000;
}
}
}
.setting-popover-slider {
--el-slider-height: 0.4rem;
height: fit-content;
.el-slider__runway {
height: var(--el-slider-height);
background-color: #e8e8e8;
border-radius: 0.2rem;
}
.el-slider__bar {
height: var(--el-slider-height);
background-color: #000;
border-radius: 0.2rem;
}
.el-slider__button-wrapper {
width: fit-content;
height: fit-content;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
}
.el-slider__button {
width: 1rem;
height: 1rem;
background-color: #000;
border-radius: 50%;
border: none;
}
.el-slider__stop {
display: none;
}
}
</style>

View File

@@ -0,0 +1,259 @@
<template>
<div class="fida-style-select-wrapper" :class="{ 'is-disabled': disabled }">
<el-select
:model-value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
@focus="openStylePopup"
/>
<el-popover
v-model:visible="stylePopupVisible"
:disabled="disabled"
placement="top"
:width="342"
:show-arrow="false"
trigger="click"
popper-class="fida-style-select-popover"
>
<template #reference>
<div class="fida-style-select-trigger"></div>
</template>
<div class="fida-style-popover-content flex flex-col">
<div class="fida-style-popover-header">
{{ title }}
</div>
<div class="fida-style-popover-grid">
<div
v-for="item in options"
:key="item.value"
class="fida-style-popover-item flex flex-center"
:class="{ 'is-selected': tempSelectedValue === item.value }"
@click="selectStyle(item.value)"
>
<img :src="getStyleImage(typeValue, item.value)" class="style-bg" />
<span class="fida-option-label flex flex-center">{{ item.label }}</span>
<img
v-show="tempSelectedValue === item.value"
src="@/assets/images/checked.png"
class="checked-item-icon"
/>
</div>
</div>
<!-- <div class="fida-style-popover-footer flex flex-center">
<button class="fida-confirm-btn" @click="confirmStyle">
{{ confirmText }}
</button>
</div> -->
</div>
</el-popover>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { getStyleImage } from '../style'
import type { OptionItem } from './types'
const props = defineProps<{
modelValue: string
typeValue: string
options: OptionItem[]
placeholder: string
title: string
confirmText: string
disabled?: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
const tempSelectedValue = ref('')
const stylePopupVisible = ref(false)
watch(
() => props.modelValue,
(value) => {
tempSelectedValue.value = value
},
{ immediate: true }
)
watch(
() => props.disabled,
(disabled) => {
if (disabled) {
stylePopupVisible.value = false
}
}
)
const openStylePopup = () => {
if (props.disabled) return
tempSelectedValue.value = props.modelValue
stylePopupVisible.value = true
}
const selectStyle = (value: string) => {
if (props.disabled) return
tempSelectedValue.value = value
confirmStyle()
}
const confirmStyle = () => {
emit('update:modelValue', tempSelectedValue.value)
stylePopupVisible.value = false
}
</script>
<style lang="less" scoped>
.fida-style-select-wrapper {
position: relative;
width: 13.9rem;
height: 4rem;
.el-select {
width: 13.9rem;
height: 4rem;
:deep(.el-select__wrapper) {
border-radius: 0.8rem;
height: 100%;
box-shadow: none;
border: 0.1rem solid rgba(0, 0, 0, 0.1);
font-weight: 500;
font-size: 1.4rem;
min-height: initial;
.el-select__placeholder {
color: #000;
}
.el-select__icon {
color: #000;
}
&.is-disabled {
.el-select__placeholder,
.el-select__selected-item {
color: #c9c9c9;
}
}
}
}
}
.fida-style-select-trigger {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
cursor: pointer;
}
.is-disabled {
.fida-style-select-trigger {
cursor: not-allowed;
}
}
</style>
<style lang="less">
.fida-style-select-popover {
width: 34.2rem !important;
padding: 0 !important;
border-radius: 0.6rem !important;
box-shadow: 0px 5px 20px 0px rgba(0, 0, 0, 0.15) !important;
background-color: #fff !important;
border: none !important;
}
.fida-style-popover-content {
padding: 2rem 2.4rem 2.4rem;
}
.fida-style-popover-header {
font-weight: 500;
font-size: 1.6rem;
color: #000;
margin-bottom: 2rem;
padding: 2rem 2.4rem !important;
}
.fida-style-popover-grid {
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
height: 28.5rem;
overflow-y: auto;
padding-bottom: 2rem !important;
}
.fida-style-popover-item {
height: 9.1rem;
width: 9.1rem;
border-radius: 1.4rem;
cursor: pointer;
position: relative;
border: none;
.checked-item-icon {
position: absolute;
bottom: 0;
right: 0;
transform: translate(50%, 50%);
width: 2.4rem;
height: 2.4rem;
}
.style-bg {
width: 100%;
height: 100%;
border-radius: 1.4rem;
}
.fida-option-label {
font-weight: 500;
font-size: 1.2rem;
color: #fff;
text-align: center;
padding: 0.5rem;
position: absolute;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 1.4rem;
}
&.is-selected {
border: 0.3rem solid #000;
.fida-option-label {
display: none;
}
}
}
.fida-style-popover-footer {
padding: 2.4rem 0 !important;
margin-top: 2.4rem;
.fida-confirm-btn {
margin: 0 auto;
width: 15.7rem;
height: 3.4rem;
line-height: 3.4rem;
background-color: #ff7a51;
color: #fff;
border: none;
border-radius: 3.8rem;
font-weight: 500;
font-size: 1.4rem;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,48 @@
import type { OptionItem, SettingOption } from './types'
export const styleKeys: string[] = [
'Venetian Modern',
'Coastal',
'Maximalism',
'Memphis',
'Verdant',
'Century Chrome',
'Modern Revival',
'Transitional',
"Tuscan 2000's",
'Kitsch-core',
'Bauhaus',
'Constructivism',
'Nordic Noir',
'Dopamine',
'Squiggle'
]
export const optionTagOrder = ['type', 'area', 'style'] as const
export const createTypeOptions = (): OptionItem[] => [
{
label: 'Input.types.sofa',
value: 'Sofa'
},
{
label: 'Input.types.desk',
value: 'Desk'
},
{
label: 'Input.types.chair',
value: 'Chair'
}
]
export const createStyleOptions = (): OptionItem[] =>
styleKeys.map((key) => ({
label: key,
value: key
}))
export const createSettingOptions = (): SettingOption[] => [
{ label: 'Input.settingOptions.first', value: 50 },
{ label: 'Input.settingOptions.second', value: 50 },
{ label: 'Input.settingOptions.third', value: 50 }
]

View File

@@ -0,0 +1,24 @@
export interface OptionItem {
label: string
value: string
}
export interface SettingOption {
label: string
value: number
}
export interface UploadedImage {
url: string
name: string
path?: any
}
export type PreviewImage = UploadedImage | string
export type OptionTagKind = 'type' | 'area' | 'style'
export interface ParameterTag {
kind: OptionTagKind
label: string
}

View File

@@ -0,0 +1,552 @@
import { computed, nextTick, ref } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import lightIcon from '@/assets/images/light-icon.png'
import closeIcon from '@/assets/images/close-icon.png'
import TypeIcon from '@/assets/icons/TypeIcon.svg'
import RegionIcon from '@/assets/icons/RegionIcon.svg'
import StyleIcon from '@/assets/icons/StyleIcon.svg'
import restoreIcon from '@/assets/images/restore.png'
import restoreCloseIcon from '@/assets/images/tag-close.png'
import { optionTagOrder } from './options'
import type { OptionItem, OptionTagKind } from './types'
interface UseInputEditorOptions {
isAgentMode: Ref<boolean> | ComputedRef<boolean>
t: (key: string) => string
typeValue: Ref<string>
areaValue: Ref<string>
styleValue: Ref<string>
typeOptions: Ref<OptionItem[]>
areaOptions: Ref<OptionItem[]>
styleOptions: Ref<OptionItem[]>
onSubmit: () => void | Promise<void>
}
export function useInputEditor(options: UseInputEditorOptions) {
const editorRef = ref<HTMLDivElement | null>(null)
const inputValue = ref<string>('')
const reportTags = ref<HTMLElement[]>([])
const optionTags = ref<Partial<Record<OptionTagKind, HTMLElement>>>({})
const optionTagKinds = ref<OptionTagKind[]>([])
const reportPromptText = ref<Text | null>(null)
let reportTypewriterTimeout: ReturnType<typeof setTimeout> | null = null
const setEditorElement = (element: HTMLDivElement | null) => {
editorRef.value = element
}
const focusEditor = () => {
editorRef.value?.focus()
}
const stopReportTypewriter = () => {
if (reportTypewriterTimeout) {
clearTimeout(reportTypewriterTimeout)
reportTypewriterTimeout = null
}
}
const moveCaretToTextEnd = (textNode: Text) => {
const selection = window.getSelection()
if (!selection || selection.anchorNode !== textNode) return
const range = document.createRange()
range.setStart(textNode, textNode.data.length)
range.collapse(true)
selection.removeAllRanges()
selection.addRange(range)
}
const typeReportPromptText = (textNode: Text, text: string, index = 0) => {
if (reportPromptText.value !== textNode) return
if (index >= text.length) {
reportTypewriterTimeout = null
return
}
textNode.textContent = `${textNode.textContent || ''}${text.charAt(index)}`
handleEditorInput()
moveCaretToTextEnd(textNode)
reportTypewriterTimeout = setTimeout(() => {
typeReportPromptText(textNode, text, index + 1)
}, 30)
}
const removeReportPromptText = () => {
stopReportTypewriter()
const promptText = reportPromptText.value
if (promptText?.parentNode) {
const nextNode = promptText.nextSibling
if (
nextNode &&
nextNode.nodeType === Node.TEXT_NODE &&
nextNode.textContent === '\u200B'
) {
nextNode.remove()
}
promptText.parentNode.removeChild(promptText)
}
reportPromptText.value = null
}
const removeNodeWithNextSpacer = (node: Node) => {
const nextNode = node.nextSibling
if (nextNode && nextNode.nodeType === Node.TEXT_NODE && nextNode.textContent === '\u200B') {
nextNode.remove()
}
node.parentNode?.removeChild(node)
}
const hasOptionTags = () => {
return Object.values(optionTags.value).some((tag) => tag?.parentNode)
}
const getOptionTagValue = (kind: OptionTagKind) => {
if (kind === 'type') return options.typeValue.value
if (kind === 'area') return options.areaValue.value
return options.styleValue.value
}
const getOptionTagIcon = (kind: OptionTagKind) => {
if (kind === 'type') return TypeIcon
if (kind === 'area') return RegionIcon
return StyleIcon
}
const refreshOptionTagKinds = () => {
const editor = editorRef.value
if (!editor) {
optionTagKinds.value = []
return
}
optionTagKinds.value = Array.from(editor.children)
.map((child) => (child as HTMLElement).dataset.optionTagKind as OptionTagKind)
.filter(Boolean)
}
const getTranslatedOptionLabel = (items: OptionItem[], value: string) => {
const option = items.find((item) => item.value === value)
return option ? options.t(option.label) : value
}
const getOptionTagLabel = (kind: OptionTagKind) => {
const value = getOptionTagValue(kind)
if (kind === 'type') return getTranslatedOptionLabel(options.typeOptions.value, value)
if (kind === 'area') return getTranslatedOptionLabel(options.areaOptions.value, value)
return options.styleOptions.value.find((item) => item.value === value)?.label || value
}
const removeOptionTag = (kind: OptionTagKind) => {
const tag = optionTags.value[kind]
if (tag?.parentNode) {
removeNodeWithNextSpacer(tag)
}
delete optionTags.value[kind]
refreshOptionTagKinds()
}
const removeAllOptionTags = () => {
optionTagOrder.forEach(removeOptionTag)
optionTags.value = {}
optionTagKinds.value = []
}
const clearOptionTagValue = (kind: OptionTagKind) => {
if (kind === 'type') {
options.typeValue.value = ''
} else if (kind === 'area') {
options.areaValue.value = ''
} else {
options.styleValue.value = ''
}
removeOptionTag(kind)
handleEditorInput()
}
const getOptionTagInsertBefore = () => {
const editor = editorRef.value
if (!editor) return null
for (const child of Array.from(editor.childNodes)) {
if (child.nodeType === Node.ELEMENT_NODE) {
const childKind = (child as HTMLElement).dataset.optionTagKind as OptionTagKind
if (childKind) continue
}
return child
}
return null
}
const createOptionTag = (kind: OptionTagKind, label: string) => {
const tag = document.createElement('div')
tag.contentEditable = 'false'
tag.className = 'editor-tag option-tag flex-center'
tag.dataset.optionTagKind = kind
const optionIcon = getOptionTagIcon(kind) as unknown as string
const icon = document.createElement('img')
icon.className = 'option-tag-icon'
icon.src = optionIcon
icon.addEventListener('click', (ev) => {
ev.stopPropagation()
clearOptionTagValue(kind)
})
const textSpan = document.createElement('span')
textSpan.className = 'option-tag-text'
textSpan.innerText = label
tag.addEventListener('mouseenter', () => {
icon.src = closeIcon as unknown as string
icon.classList.add('option-tag-close')
tag.classList.add('is-hovered')
})
tag.addEventListener('mouseleave', () => {
icon.src = optionIcon
icon.classList.remove('option-tag-close')
tag.classList.remove('is-hovered')
})
tag.appendChild(icon)
tag.appendChild(textSpan)
return tag
}
const syncOptionTag = (kind: OptionTagKind) => {
const value = getOptionTagValue(kind)
if (!value) {
removeOptionTag(kind)
handleEditorInput()
return
}
const label = getOptionTagLabel(kind)
const existingTag = optionTags.value[kind]
if (existingTag?.parentNode) {
const text = existingTag.querySelector('.option-tag-text')
if (text) text.textContent = label
handleEditorInput()
return
}
const editor = editorRef.value
if (!editor) return
const tag = createOptionTag(kind, label)
const referenceNode = getOptionTagInsertBefore()
if (referenceNode) {
editor.insertBefore(tag, referenceNode)
} else {
editor.appendChild(tag)
}
if (tag.parentNode) {
tag.parentNode.insertBefore(document.createTextNode('\u200B'), tag.nextSibling)
}
optionTags.value[kind] = tag
refreshOptionTagKinds()
handleEditorInput()
}
const syncAllOptionTags = () => {
optionTagOrder.forEach(syncOptionTag)
}
const showPlaceholder = computed(() => {
if (!editorRef.value) return true
if (inputValue.value || reportTags.value.length > 0 || hasOptionTags()) return false
const editor = editorRef.value
const textContent = editor.textContent?.replace(/[\s\u200B\n]/g, '').trim() || ''
const isEmptyHTML = editor.innerHTML === '' || editor.innerHTML === '<br>'
const hasMeaningfulChildren = Array.from(editor.children).some(
(child) => child.tagName !== 'BR' && !child.classList.contains('editor-tag')
)
return textContent === '' && !hasMeaningfulChildren && isEmptyHTML
})
const addReportTag = (text?: string) => {
const tagText = text || options.t('Input.trendingReport')
const tag = document.createElement('div')
tag.contentEditable = 'false'
const imgLeft = document.createElement('img')
const imgClose = document.createElement('img')
const textSpan = document.createElement('span')
imgClose.className = 'close-icon'
if (text) {
tag.className = 'editor-tag restore flex-center'
imgLeft.className = 'restore-icon'
imgLeft.src = restoreIcon as unknown as string
imgClose.src = restoreCloseIcon as unknown as string
imgClose.className = 'close-icon restore'
textSpan.className = 'restore-text'
} else {
tag.className = 'editor-tag report-btn flex-center'
imgLeft.className = 'light-icon'
imgLeft.src = lightIcon as unknown as string
imgClose.src = closeIcon as unknown as string
}
textSpan.innerText = tagText
imgClose.addEventListener('click', (ev) => {
ev.stopPropagation()
removeReportPromptText()
tag.remove()
const idx = reportTags.value.indexOf(tag)
if (idx > -1) reportTags.value.splice(idx, 1)
handleEditorInput()
})
tag.appendChild(imgLeft)
tag.appendChild(textSpan)
tag.appendChild(imgClose)
const selection = window.getSelection()
const isInEditor =
editorRef.value && selection && editorRef.value.contains(selection.anchorNode)
if (isInEditor && selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
range.insertNode(tag)
focusEditor()
} else if (editorRef.value) {
editorRef.value.appendChild(tag)
focusEditor()
}
reportTags.value.push(tag)
const promptText = document.createTextNode('')
if (tag.parentNode) {
tag.parentNode.insertBefore(promptText, tag.nextSibling)
const zwsp = document.createTextNode('\u200B')
tag.parentNode.insertBefore(zwsp, promptText.nextSibling)
const newRange = document.createRange()
newRange.setStart(promptText, promptText.data.length)
newRange.collapse(true)
const currentSelection = window.getSelection()
currentSelection?.removeAllRanges()
currentSelection?.addRange(newRange)
}
reportPromptText.value = promptText
typeReportPromptText(promptText, options.t('Input.reportPlaceholder'))
}
const toggleReportTag = (clear = false) => {
const shouldClear = clear === true
reportTags.value = reportTags.value.filter((tag) => tag.parentNode !== null)
if (shouldClear) {
removeReportPromptText()
reportTags.value.forEach((tag) => {
tag.remove()
})
reportTags.value = []
return
}
if (reportTags.value.length > 0) {
removeReportPromptText()
reportTags.value.forEach((tag) => {
if (
tag.nextSibling &&
tag.nextSibling.nodeType === Node.TEXT_NODE &&
tag.nextSibling.textContent === '\u200B'
) {
tag.nextSibling.remove()
}
tag.remove()
})
reportTags.value = []
handleEditorInput()
} else {
addReportTag()
}
}
const cleanupEditor = () => {
if (!editorRef.value) return
const editor = editorRef.value
if (editor.textContent) {
const cleanedText = editor.textContent.replace(/[\u200B-\u200D\uFEFF]/g, '')
if (!cleanedText.trim() && editor.children.length === 0) {
editor.innerHTML = ''
editor.textContent = ''
reportTags.value = []
removeAllOptionTags()
stopReportTypewriter()
reportPromptText.value = null
}
}
}
const autoResizeEditor = () => {
if (options.isAgentMode.value) return
}
const handleEditorInput = () => {
if (!editorRef.value) return
let text = ''
const walker = document.createTreeWalker(editorRef.value, NodeFilter.SHOW_TEXT, null)
let node: Node | null
while ((node = walker.nextNode())) {
if (node.parentElement?.closest('.editor-tag')) continue
text += node.textContent
}
text = text.replace(/[\s\u200B]+$/, '')
inputValue.value = text
const editor = editorRef.value
const hasChildElements = editor.children.length > 0
const hasTextContent = editor.textContent?.replace(/[\s\u200B]/g, '').trim().length > 0
if (!hasChildElements && !hasTextContent) {
editor.innerHTML = ''
reportTags.value = []
removeAllOptionTags()
stopReportTypewriter()
reportPromptText.value = null
}
autoResizeEditor()
}
const handleEditorPaste = (e: ClipboardEvent) => {
e.preventDefault()
const text = e.clipboardData?.getData('text/plain') || ''
document.execCommand('insertText', false, text)
}
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
options.onSubmit()
return
}
if (e.key !== 'Backspace') return
const selection = window.getSelection()
if (!selection || selection.rangeCount <= 0) return
const range = selection.getRangeAt(0)
if (!range.collapsed) return
let nodeToDelete: Node | null = null
const startContainer = range.startContainer
const startOffset = range.startOffset
if (startContainer.nodeType === Node.TEXT_NODE) {
const textNode = startContainer as Text
if (startOffset === textNode.length) {
nodeToDelete = textNode.nextSibling
}
} else if (startContainer.nodeType === Node.ELEMENT_NODE) {
nodeToDelete = startContainer.childNodes[startOffset]
}
if (
nodeToDelete &&
nodeToDelete.nodeType === Node.ELEMENT_NODE &&
(nodeToDelete as Element).classList
) {
const element = nodeToDelete as Element
const isEditorTag = element.classList.contains('editor-tag')
const isReportTag = element.classList.contains('report-tag')
const optionTagKind = (element as HTMLElement).dataset.optionTagKind as OptionTagKind
if (optionTagKind) {
e.preventDefault()
clearOptionTagValue(optionTagKind)
nextTick(() => {
cleanupEditor()
handleEditorInput()
})
return
}
if (isEditorTag || isReportTag) {
e.preventDefault()
if (reportTags.value.includes(element as HTMLElement)) {
removeReportPromptText()
}
element.remove()
const index = reportTags.value.indexOf(nodeToDelete as HTMLElement)
if (index > -1) {
reportTags.value.splice(index, 1)
}
nextTick(() => {
cleanupEditor()
handleEditorInput()
})
return
}
}
nextTick(() => {
if (editorRef.value) {
const text = editorRef.value.textContent || ''
const cleanedText = text.replace(/[\u200B-\u200D\uFEFF]/g, '')
if (editorRef.value.innerHTML === '<br>' || !cleanedText.trim()) {
editorRef.value.innerHTML = ''
if (editorRef.value.children.length === 0) {
reportTags.value = []
reportPromptText.value = null
}
}
handleEditorInput()
}
})
}
const clearEditorText = () => {
if (editorRef.value) {
editorRef.value.innerHTML = ''
}
inputValue.value = ''
}
const resetOptionTags = () => {
optionTags.value = {}
optionTagKinds.value = []
}
return {
editorRef,
inputValue,
reportTags,
optionTagKinds,
showPlaceholder,
setEditorElement,
focusEditor,
stopReportTypewriter,
addReportTag,
toggleReportTag,
handleEditorInput,
handleEditorPaste,
handleKeyDown,
autoResizeEditor,
syncOptionTag,
syncAllOptionTags,
clearEditorText,
resetOptionTags
}
}

View File

@@ -0,0 +1,78 @@
import { nextTick, ref } from 'vue'
import { uploadImage } from '@/api/upload'
import type { PreviewImage, UploadedImage } from './types'
export function useInputImages(focusEditor: () => void) {
const uploadedImages = ref<UploadedImage[]>([])
const quoteList = ref<string[]>([])
const showPreview = ref(false)
const previewUrl = ref('')
const handleFileChange = (event: Event) => {
const input = event.target as HTMLInputElement
if (input.files) {
Array.from(input.files).forEach((file) => {
if (file.type.startsWith('image/')) {
const formData = new FormData()
formData.append('file', file)
uploadImage(formData).then((res) => {
const reader = new FileReader()
reader.onload = (e) => {
uploadedImages.value.push({
url: e.target?.result as string,
name: file.name,
path: res
})
}
reader.readAsDataURL(file)
})
}
})
}
nextTick(focusEditor)
input.value = ''
}
const removeImage = (index: number, item: PreviewImage) => {
if (typeof item === 'string') {
const quoteIndex = quoteList.value.indexOf(item)
if (quoteIndex > -1) {
quoteList.value.splice(quoteIndex, 1)
}
return
}
uploadedImages.value.splice(index, 1)
}
const previewImage = (url: string) => {
showPreview.value = true
previewUrl.value = url
}
const handleQuote = (url: string) => {
const hasQuoted = quoteList.value.includes(url)
if (hasQuoted) return
quoteList.value[0] = url
}
const clearImages = () => {
uploadedImages.value = []
quoteList.value = []
}
return {
uploadedImages,
quoteList,
showPreview,
previewUrl,
handleFileChange,
removeImage,
previewImage,
handleQuote,
clearImages
}
}

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="main-input-container flex-1"> <div class="main-input-container flex-1">
<div class="slogan"> <div class="slogan">
<p>Creating Things with <span class="fiDA">FiDA</span> that</p> <p>Creating Works with <span class="fiDA">FiDA</span> that</p>
<p>Bloom Your Creativity</p> <p>Bloom Your Creativity</p>
</div> </div>
<Input /> <Input />

View File

@@ -13,7 +13,11 @@
const url = const url =
'https://www.minio-api.aida.com.hk/fida-user/2/d8512e53-f016-4ad6-8245-2f304d89e7b2.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T032733Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=25e5ec227a0ca22942e71eff3a4f07a23f8812ff3db5522e1466b3a77288be70' 'https://www.minio-api.aida.com.hk/fida-user/2/d8512e53-f016-4ad6-8245-2f304d89e7b2.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20260331%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260331T032733Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=25e5ec227a0ca22942e71eff3a4f07a23f8812ff3db5522e1466b3a77288be70'
const openCanvas = () => { const openCanvas = () => {
myEvent.emit('openFlowCanvas', { url }) myEvent.emit('openFlowCanvas', {
url,
imgId: '69bcaae11e0cee430b750050',
nodeId: '69cde574a510db41350b404c'
})
} }
const openDepthCanvas = () => { const openDepthCanvas = () => {
myEvent.emit('openDepthCanvas', { myEvent.emit('openDepthCanvas', {

View File

@@ -127,6 +127,7 @@
if (res) { if (res) {
userInfoStore.setToken(res) userInfoStore.setToken(res)
userInfoStore.setUserInfo({ userInfoStore.setUserInfo({
username: formData.name,
email: formData.email email: formData.email
}) })
router.push({ name: 'nuic' }) router.push({ name: 'nuic' })

View File

@@ -62,7 +62,7 @@ export default defineConfig(({ mode }) => {
host: '0.0.0.0', // 允许局域网内的IP访问 host: '0.0.0.0', // 允许局域网内的IP访问
port: 8060, // 根据环境设置端口 port: 8060, // 根据环境设置端口
open: false, // 自动打开浏览器 open: false, // 自动打开浏览器
strictPort: true, // 如果端口已被占用,则尝试下一个可用端口 strictPort: false, // 如果端口已被占用,则尝试下一个可用端口
hmr: { hmr: {
overlay: true overlay: true
}, },