Compare commits
60 Commits
a3abe2e083
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8f7d157f4 | ||
| 8acb5b4ce5 | |||
| 1dd36b1b8c | |||
| 2bca08ed97 | |||
| ba63d16d60 | |||
|
|
f24a9afe5c | ||
| 4d1d082fae | |||
| c2f0f82218 | |||
| 620962b9ee | |||
| 89aab7e960 | |||
| 1078961608 | |||
| d073008736 | |||
| 26cd16be09 | |||
| 6098993bb3 | |||
| c3d26bdb49 | |||
| b55a5ba896 | |||
| 5097e71311 | |||
|
|
2baff3be45 | ||
| 0b8b4c7aeb | |||
| 479b95e208 | |||
| a8050f8065 | |||
| 794b7c5fc1 | |||
| fae2e21ca6 | |||
|
|
5e6d53bbe6 | ||
|
|
a649f1fc63 | ||
|
|
d4d9c2eede | ||
| 8cc83ae12b | |||
| 477c3bfa79 | |||
|
|
6517560bd8 | ||
|
|
b4fc3addb8 | ||
| 25b19a3a88 | |||
| 775a4f816e | |||
|
|
da9527ff11 | ||
|
|
3659cb6137 | ||
| c992b6951b | |||
| 3e7d5a0403 | |||
| 82c10744fb | |||
| 91bf90e948 | |||
|
|
1f9b923128 | ||
|
|
ba50072cf9 | ||
| e96eb5b30b | |||
| f0932c68db | |||
| 482e7c77a1 | |||
| 85a9e79329 | |||
|
|
85b47e3654 | ||
|
|
1aa9119f43 | ||
|
|
e3593f9e96 | ||
| 9cb55365a1 | |||
| 1301fd3825 | |||
| f34ab656a1 | |||
| 8b39f5b70e | |||
|
|
b16b41f44e | ||
|
|
93d2eb0696 | ||
|
|
254352bf9a | ||
|
|
110b39918e | ||
| 8806fbeb49 | |||
| c8fc36d1cc | |||
| 80c0ebc4d0 | |||
| e3d7f58224 | |||
| 426f05ba60 |
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.svg">
|
||||||
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
|
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
|
||||||
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> -->
|
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> -->
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
|
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
|
||||||
|
|||||||
4
public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="753" height="753" viewBox="0 0 753 753" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M634.536 365.274L634.824 364.392L697.737 132.952H631.864L613.396 200.848H363.698L360.43 201.55C272.317 220.226 254.644 306.834 253.94 310.508L253.362 313.497V419.411C180.124 419.159 122.772 418.871 121.382 418.835L121.039 482.264C126.671 482.3 182.651 482.606 253.38 482.84V632.625H316.96V483.038C345.627 483.11 374.98 483.128 403.051 483.128C467.299 483.128 461.451 482.912 489.865 482.264C493.114 482.192 496.255 482.012 499.378 481.76L608.071 632.625H686.364L562.128 460.166C613.089 427.209 633.434 368.678 634.572 365.274H634.536ZM488.403 418.871C448.579 419.753 411.121 419.789 316.942 419.573V320.251C319.668 310.255 332.142 274.11 370.702 264.277H596.175L574.042 345.752C571.028 353.928 545.845 417.574 488.403 418.889V418.871Z" fill="black"/>
|
||||||
|
<path d="M91.0127 418.726H54.2764V482.498H91.0127V418.726Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 942 B |
@@ -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 线稿图上色的参数
|
||||||
@@ -205,11 +238,13 @@ export const sketchAddPrintApi = (data:sketchAddPrintData) => {
|
|||||||
* @param data 图片转3d的参数
|
* @param data 图片转3d的参数
|
||||||
* @param data.sketchId sketch id
|
* @param data.sketchId sketch id
|
||||||
* @param data.imageUrls 进行生成的图片。minio地址和正常地址都可以
|
* @param data.imageUrls 进行生成的图片。minio地址和正常地址都可以
|
||||||
|
* @param data.mode 选择的模型
|
||||||
* @returns 图片转3d
|
* @returns 图片转3d
|
||||||
*/
|
*/
|
||||||
export interface sketchToThreeData {
|
export interface sketchToThreeData {
|
||||||
sketchId?: string
|
sketchId?: string
|
||||||
imageUrls?: Array<string>
|
imageUrls?: Array<string>
|
||||||
|
mode?: string
|
||||||
}
|
}
|
||||||
export const sketchToThreeApi = (data:sketchToThreeData) => {
|
export const sketchToThreeApi = (data:sketchToThreeData) => {
|
||||||
return request({
|
return request({
|
||||||
@@ -218,6 +253,7 @@ export const sketchToThreeApi = (data:sketchToThreeData) => {
|
|||||||
data:{
|
data:{
|
||||||
sketchId: data.sketchId,
|
sketchId: data.sketchId,
|
||||||
imageUrls: data.imageUrls,
|
imageUrls: data.imageUrls,
|
||||||
|
mode: data.mode,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -243,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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
3
src/assets/icons/RegionIcon.svg
Normal 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 |
7
src/assets/icons/StyleIcon.svg
Normal 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 |
10
src/assets/icons/TypeIcon.svg
Normal 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 |
3
src/assets/icons/captureView.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.99997 1.15789C5.33447 1.15789 4.75255 1.50608 4.44041 2.02674C4.33317 2.20563 4.13523 2.31579 3.92105 2.31579H3.48C2.96605 2.31579 2.61668 2.31624 2.34662 2.33753C2.08357 2.35827 1.94905 2.39586 1.85521 2.44199C1.62942 2.553 1.44584 2.73014 1.33079 2.94801C1.28298 3.03856 1.24402 3.16836 1.22253 3.42218C1.20047 3.68276 1.2 4.01988 1.2 4.51579V7.64211C1.2 8.13802 1.20047 8.47514 1.22253 8.73571C1.24402 8.98953 1.28298 9.11934 1.33079 9.20988C1.44584 9.42776 1.62942 9.60489 1.85521 9.7159C1.94905 9.76204 2.08357 9.79963 2.34662 9.82036C2.61668 9.84165 2.96605 9.8421 3.48 9.8421H8.52C9.03395 9.8421 9.38332 9.84165 9.65338 9.82036C9.91643 9.79963 10.051 9.76204 10.1448 9.7159C10.3706 9.60489 10.5542 9.42776 10.6692 9.20988C10.717 9.11934 10.756 8.98953 10.7775 8.73571C10.7995 8.47514 10.8 8.13802 10.8 7.64211V4.51579C10.8 4.01988 10.7995 3.68276 10.7775 3.42218C10.756 3.16836 10.717 3.03856 10.6692 2.94801C10.5542 2.73014 10.3706 2.553 10.1448 2.44199C10.051 2.39586 9.91643 2.35827 9.65338 2.33753C9.38332 2.31624 9.03395 2.31579 8.52 2.31579H8.07889C7.86472 2.31579 7.66678 2.20563 7.55953 2.02674C7.2474 1.50608 6.66547 1.15789 5.99997 1.15789ZM3.59984 1.15789C4.1465 0.455643 5.01782 0 5.99997 0C6.98213 0 7.85344 0.455643 8.40011 1.15789L8.54478 1.15789C9.02776 1.15789 9.42638 1.15788 9.7511 1.18348C10.0884 1.21007 10.3984 1.26713 10.6896 1.4103C11.1412 1.63232 11.5083 1.98659 11.7384 2.42234C11.8868 2.70332 11.9459 3.00247 11.9735 3.32789C12 3.64121 12 4.02584 12 4.49187V7.66603C12 8.13206 12 8.51668 11.9735 8.83C11.9459 9.15543 11.8868 9.45457 11.7384 9.73556C11.5083 10.1713 11.1412 10.5256 10.6896 10.7476C10.3984 10.8908 10.0884 10.9478 9.7511 10.9744C9.42638 11 9.02777 11 8.5448 11H3.45521C2.97223 11 2.57362 11 2.2489 10.9744C1.91165 10.9478 1.60162 10.8908 1.31042 10.7476C0.858835 10.5256 0.491681 10.1713 0.261585 9.73556C0.11321 9.45457 0.0540718 9.15543 0.0265167 8.83C-1.36882e-05 8.51668 -7.39446e-06 8.13205 2.58775e-07 7.66602V4.49188C-7.39446e-06 4.02584 -1.36882e-05 3.64121 0.0265167 3.32789C0.0540718 3.00247 0.11321 2.70332 0.261585 2.42234C0.491681 1.98659 0.858834 1.63232 1.31042 1.4103C1.60162 1.26713 1.91165 1.21007 2.2489 1.18348C2.57362 1.15788 2.97224 1.15789 3.45522 1.15789L3.59984 1.15789ZM5.99997 4.63158C5.25439 4.63158 4.64997 5.21479 4.64997 5.93421C4.64997 6.65363 5.25439 7.23684 5.99997 7.23684C6.74556 7.23684 7.34997 6.65363 7.34997 5.93421C7.34997 5.21479 6.74556 4.63158 5.99997 4.63158ZM3.44997 5.93421C3.44997 4.5753 4.59165 3.47368 5.99997 3.47368C7.4083 3.47368 8.54997 4.5753 8.54997 5.93421C8.54997 7.29312 7.4083 8.39474 5.99997 8.39474C4.59165 8.39474 3.44997 7.29312 3.44997 5.93421Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
6
src/assets/icons/to-real-variants-2.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1959 2.6173C12.4159 1.8333 11.1479 1.8333 10.3679 2.6173L3.01592 9.9693C2.23192 10.7493 2.23192 12.0173 3.01592 12.7973C3.79592 13.5813 5.06392 13.5813 5.84392 12.7973L13.1959 5.4453C13.9799 4.6653 13.9799 3.3973 13.1959 2.6173ZM12.5319 4.5213L11.1159 5.9333C11.0399 6.0133 10.9119 6.0133 10.8359 5.9333L10.2679 5.3693C10.1919 5.2893 10.1919 5.1613 10.2679 5.0853L11.6839 3.6693C11.7599 3.5933 11.8879 3.5933 11.9679 3.6693L12.5319 4.2373C12.6079 4.3133 12.6079 4.4413 12.5319 4.5213Z" fill="black"/>
|
||||||
|
<path d="M7.59998 3.6C8.04181 3.6 8.39998 3.24183 8.39998 2.8C8.39998 2.35817 8.04181 2 7.59998 2C7.15815 2 6.79998 2.35817 6.79998 2.8C6.79998 3.24183 7.15815 3.6 7.59998 3.6Z" fill="#FF7A51"/>
|
||||||
|
<path d="M11.624 9.412C11.752 9.064 12.244 9.064 12.376 9.412L12.8 10.56C12.84 10.668 12.928 10.756 13.036 10.796L14.184 11.22C14.532 11.348 14.532 11.84 14.184 11.972L13.036 12.396C12.928 12.436 12.84 12.524 12.8 12.632L12.376 13.78C12.248 14.128 11.756 14.128 11.624 13.78L11.2 12.632C11.16 12.524 11.072 12.436 10.964 12.396L9.81598 11.972C9.46798 11.844 9.46798 11.352 9.81598 11.22L10.964 10.796C11.072 10.756 11.16 10.668 11.2 10.56L11.624 9.412Z" fill="#FF7A51"/>
|
||||||
|
<path d="M3.62398 3.012C3.75198 2.664 4.24398 2.664 4.37598 3.012L4.58398 3.576C4.62398 3.684 4.71198 3.772 4.81998 3.812L5.38398 4.02C5.73198 4.148 5.73198 4.64 5.38398 4.772L4.81998 4.98C4.71198 5.02 4.62398 5.108 4.58398 5.216L4.37598 5.78C4.24798 6.128 3.75598 6.128 3.62398 5.78L3.41598 5.216C3.37598 5.108 3.28798 5.02 3.17998 4.98L2.61598 4.772C2.26798 4.644 2.26798 4.152 2.61598 4.02L3.17998 3.812C3.28798 3.772 3.37598 3.684 3.41598 3.576L3.62398 3.012Z" fill="#FF7A51"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
6
src/assets/icons/to-real-variants.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4956 3.27113C15.5206 2.29113 13.9356 2.29113 12.9606 3.27113L3.77064 12.4611C2.79064 13.4361 2.79064 15.0211 3.77064 15.9961C4.74564 16.9761 6.33064 16.9761 7.30564 15.9961L16.4956 6.80613C17.4756 5.83113 17.4756 4.24613 16.4956 3.27113ZM15.6656 5.65113L13.8956 7.41613C13.8006 7.51613 13.6406 7.51613 13.5456 7.41613L12.8356 6.71113C12.7406 6.61113 12.7406 6.45113 12.8356 6.35613L14.6056 4.58613C14.7006 4.49113 14.8606 4.49113 14.9606 4.58613L15.6656 5.29613C15.7606 5.39113 15.7606 5.55113 15.6656 5.65113Z" fill="white"/>
|
||||||
|
<path d="M9.50059 4.5C10.0529 4.5 10.5006 4.05228 10.5006 3.5C10.5006 2.94772 10.0529 2.5 9.50059 2.5C8.9483 2.5 8.50059 2.94772 8.50059 3.5C8.50059 4.05228 8.9483 4.5 9.50059 4.5Z" fill="white"/>
|
||||||
|
<path d="M14.5306 11.765C14.6906 11.33 15.3056 11.33 15.4706 11.765L16.0006 13.2C16.0506 13.335 16.1606 13.445 16.2956 13.495L17.7306 14.025C18.1656 14.185 18.1656 14.8 17.7306 14.965L16.2956 15.495C16.1606 15.545 16.0506 15.655 16.0006 15.79L15.4706 17.225C15.3106 17.66 14.6956 17.66 14.5306 17.225L14.0006 15.79C13.9506 15.655 13.8406 15.545 13.7056 15.495L12.2706 14.965C11.8356 14.805 11.8356 14.19 12.2706 14.025L13.7056 13.495C13.8406 13.445 13.9506 13.335 14.0006 13.2L14.5306 11.765Z" fill="white"/>
|
||||||
|
<path d="M4.53059 3.765C4.69059 3.33 5.30559 3.33 5.47059 3.765L5.73059 4.47C5.78059 4.605 5.89059 4.715 6.02559 4.765L6.73059 5.025C7.16559 5.185 7.16559 5.8 6.73059 5.965L6.02559 6.225C5.89059 6.275 5.78059 6.385 5.73059 6.52L5.47059 7.225C5.31059 7.66 4.69559 7.66 4.53059 7.225L4.27059 6.52C4.22059 6.385 4.11059 6.275 3.97559 6.225L3.27059 5.965C2.83559 5.805 2.83559 5.19 3.27059 5.025L3.97559 4.765C4.11059 4.715 4.22059 4.605 4.27059 4.47L4.53059 3.765Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -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;
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ export class LayerManager {
|
|||||||
})
|
})
|
||||||
this.setLayerPosition(rectObject, options)
|
this.setLayerPosition(rectObject, options)
|
||||||
await this.canvasManager.add(rectObject, isRecord)
|
await this.canvasManager.add(rectObject, isRecord)
|
||||||
if (isActive) this.setActiveID(rectObject.info.id)
|
if (isActive) this.setActiveID(rectObject.info.id, false)
|
||||||
return rectObject
|
return rectObject
|
||||||
}
|
}
|
||||||
/** 创建直线图层 */
|
/** 创建直线图层 */
|
||||||
@@ -303,7 +303,7 @@ export class LayerManager {
|
|||||||
});
|
});
|
||||||
this.setLayerPosition(arrowObject, options)
|
this.setLayerPosition(arrowObject, options)
|
||||||
await this.canvasManager.add(arrowObject, isRecord)
|
await this.canvasManager.add(arrowObject, isRecord)
|
||||||
if (isActive) this.setActiveID(arrowObject.info.id)
|
if (isActive) this.setActiveID(arrowObject.info.id, false)
|
||||||
return arrowObject
|
return arrowObject
|
||||||
}
|
}
|
||||||
/** 创建椭圆图层 */
|
/** 创建椭圆图层 */
|
||||||
@@ -320,7 +320,7 @@ export class LayerManager {
|
|||||||
})
|
})
|
||||||
this.setLayerPosition(ellipseObject, options)
|
this.setLayerPosition(ellipseObject, options)
|
||||||
await this.canvasManager.add(ellipseObject)
|
await this.canvasManager.add(ellipseObject)
|
||||||
if (isActive) this.setActiveID(ellipseObject.info.id)
|
if (isActive) this.setActiveID(ellipseObject.info.id, false)
|
||||||
return ellipseObject
|
return ellipseObject
|
||||||
}
|
}
|
||||||
/** 创建三角形图层 */
|
/** 创建三角形图层 */
|
||||||
@@ -339,7 +339,7 @@ export class LayerManager {
|
|||||||
})
|
})
|
||||||
this.setLayerPosition(triangleObject, options)
|
this.setLayerPosition(triangleObject, options)
|
||||||
await this.canvasManager.add(triangleObject, isRecord)
|
await this.canvasManager.add(triangleObject, isRecord)
|
||||||
if (isActive) this.setActiveID(triangleObject.info.id)
|
if (isActive) this.setActiveID(triangleObject.info.id, false)
|
||||||
return triangleObject
|
return triangleObject
|
||||||
}
|
}
|
||||||
/** 创建五角星图层 */
|
/** 创建五角星图层 */
|
||||||
@@ -360,7 +360,7 @@ export class LayerManager {
|
|||||||
})
|
})
|
||||||
this.setLayerPosition(starObject, options)
|
this.setLayerPosition(starObject, options)
|
||||||
await this.canvasManager.add(starObject, isRecord)
|
await this.canvasManager.add(starObject, isRecord)
|
||||||
if (isActive) this.setActiveID(starObject.info.id)
|
if (isActive) this.setActiveID(starObject.info.id, false)
|
||||||
return starObject
|
return starObject
|
||||||
}
|
}
|
||||||
/** 创建箭头图层 */
|
/** 创建箭头图层 */
|
||||||
@@ -384,7 +384,7 @@ export class LayerManager {
|
|||||||
});
|
});
|
||||||
this.setLayerPosition(arrowObject, options)
|
this.setLayerPosition(arrowObject, options)
|
||||||
await this.canvasManager.add(arrowObject, isRecord)
|
await this.canvasManager.add(arrowObject, isRecord)
|
||||||
if (isActive) this.setActiveID(arrowObject.info.id)
|
if (isActive) this.setActiveID(arrowObject.info.id, false)
|
||||||
return arrowObject
|
return arrowObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -217,11 +217,12 @@ export class ObjectManager {
|
|||||||
const object = this.canvasManager.getObjectById(id)
|
const object = this.canvasManager.getObjectById(id)
|
||||||
if (!object) return null
|
if (!object) return null
|
||||||
const type = object.type
|
const type = object.type
|
||||||
const isWidth = object.hasOwnProperty('width')
|
const isWidth = options.hasOwnProperty('width')
|
||||||
const isHeight = object.hasOwnProperty('height')
|
const isHeight = options.hasOwnProperty('height')
|
||||||
if (isWidth || isHeight) {
|
if (isWidth || isHeight) {
|
||||||
const width = isWidth ? options.width : object.width
|
const width = isWidth ? options.width : object.width
|
||||||
const height = isHeight ? options.height : object.height
|
const height = isHeight ? options.height : object.height
|
||||||
|
console.log(width, height)
|
||||||
if (type === "polygon") {
|
if (type === "polygon") {
|
||||||
if (object.points.length === 10) {// 五角星
|
if (object.points.length === 10) {// 五角星
|
||||||
options.points = getStarArr(width, height)
|
options.points = getStarArr(width, height)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 高级工具选择 -->
|
<!-- 高级工具选择 -->
|
||||||
<div class="cards-select">
|
<div class="cards-select">
|
||||||
<div v-for="v in (node.data?.secondaryMenu?.selectList || list)" :key="v.type" @click="onClickItem(v)" v-show="v.tier === tier">
|
<div
|
||||||
|
v-for="v in (node.data?.secondaryMenu?.selectList || list)"
|
||||||
|
:key="v.type"
|
||||||
|
@click="onClickItem(v)"
|
||||||
|
v-show="
|
||||||
|
(v.tier === tier) &&
|
||||||
|
((v.type == NODE_DATATYPE.TO_REAL_VARIANTS && (superiorNode?.data?.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL && props.tier == NODE_DATATIER.TO_REAL_VARIANTS))||
|
||||||
|
(v.type != NODE_DATATYPE.TO_REAL_VARIANTS && (superiorNode?.data?.superiorNodeType != NODE_DATATYPE.TO_3D_MODEL || props.tier != NODE_DATATIER.TO_REAL_VARIANTS)))
|
||||||
|
"
|
||||||
|
>
|
||||||
<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>
|
||||||
@@ -13,34 +22,63 @@
|
|||||||
<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({
|
||||||
node: { required: true, type: Object },
|
node: { required: true, type: Object },
|
||||||
tier: { default: 1, type: Number }
|
tier: { default: 1, type: Number }
|
||||||
})
|
})
|
||||||
|
const superiorNode = computed(() => {
|
||||||
|
return stateManager.getNodeById(props.node.data.superiorID)
|
||||||
|
})
|
||||||
const list = ref([
|
const list = ref([
|
||||||
{
|
{
|
||||||
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,
|
||||||
|
type: NODE_DATATYPE.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'),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -48,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) => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
import { computed, ref, useAttrs, onMounted, inject, watch } from 'vue'
|
import { computed, ref, useAttrs, onMounted, inject, watch } from 'vue'
|
||||||
import CardsSelect from './cards-select.vue'
|
import CardsSelect from './cards-select.vue'
|
||||||
import ToRealStyle from './to-real-style.vue'
|
import ToRealStyle from './to-real-style.vue'
|
||||||
|
import ToRealVariants from './to-real-variants.vue'
|
||||||
import SurfaceEdit from './surface-edit.vue'
|
import SurfaceEdit from './surface-edit.vue'
|
||||||
import FastMode from './fast-mode.vue'
|
import FastMode from './fast-mode.vue'
|
||||||
import SceneComposition from './scene-composition.vue'
|
import SceneComposition from './scene-composition.vue'
|
||||||
@@ -45,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'
|
||||||
@@ -57,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,
|
||||||
@@ -65,53 +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,
|
||||||
|
type: NODE_DATATYPE.TO_REAL_VARIANTS,
|
||||||
|
title: t('FlowCanvas.toRealVariantsTitle'),
|
||||||
|
component: ToRealVariants,
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -147,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)
|
||||||
@@ -167,15 +194,20 @@
|
|||||||
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){
|
||||||
|
//一个结果节点里面多个子节点
|
||||||
let imageProcessTasks = taskList
|
let imageProcessTasks = taskList
|
||||||
nodeManager.createResultNode({
|
nodeManager.createResultNode({
|
||||||
data: {
|
data: {
|
||||||
@@ -196,7 +228,8 @@
|
|||||||
data: {
|
data: {
|
||||||
superiorID: attrs.node.id,
|
superiorID: attrs.node.id,
|
||||||
superiorNodeType: attrs.node?.data?.type,
|
superiorNodeType: attrs.node?.data?.type,
|
||||||
createIndexPosition: index + subordNodes.length,
|
superiorGenerateImg: superiorNodeUrl || null,
|
||||||
|
createIndexPosition: index + maxPosition,
|
||||||
tier: tier,
|
tier: tier,
|
||||||
isActive: index == 0 && subordNodes.length == 0,
|
isActive: index == 0 && subordNodes.length == 0,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -1,24 +1,35 @@
|
|||||||
<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">{{ $t('FlowCanvas.mode') }}</p>
|
||||||
|
<my-select v-model="data.mode" :list="modeList" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, inject, useAttrs, computed } 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: 'Normal',
|
||||||
})
|
})
|
||||||
|
const modeList = ref([
|
||||||
|
{ value: 'Advanced', label: t('FlowCanvas.advancedMode') },
|
||||||
|
{ value: 'Normal', label: t('FlowCanvas.normalMode') }
|
||||||
|
])
|
||||||
const getApiData = ()=>{
|
const getApiData = ()=>{
|
||||||
return {
|
return {
|
||||||
imageUrls: [data.url],
|
imageUrls: [data.url],
|
||||||
|
mode: data.mode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defineExpose({ data,getApiData })
|
defineExpose({ data,getApiData })
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 转换为真实图 -->
|
||||||
|
<div class="to-real-style">
|
||||||
|
<p class="label">{{ t('FlowCanvas.prompt') }}</p>
|
||||||
|
<my-textarea v-model="data.prompt" :placeholder="$t('FlowCanvas.toRealVariantsPlaceholder')" />
|
||||||
|
<div class="shortcut-list">
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-for="v in shortcutList"
|
||||||
|
:key="v.value"
|
||||||
|
@click="data.prompt = v.value"
|
||||||
|
>
|
||||||
|
{{ v.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <p class="label">{{ t('FlowCanvas.mode') }}</p>
|
||||||
|
<my-select v-model="data.mode" :list="modeList" /> -->
|
||||||
|
<p class="label">{{ t('FlowCanvas.size') }}</p>
|
||||||
|
<pixel-ratio-selection v-model="data.pixelRatio" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, ref, reactive, useAttrs } from 'vue'
|
||||||
|
import myTextarea from '../../tools/my-textarea.vue'
|
||||||
|
import mySelect from '../../tools/my-select.vue'
|
||||||
|
import pixelRatioSelection from '../../tools/pixel-ratio-selection.vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
const { t } = useI18n()
|
||||||
|
const shortcutList = ref([
|
||||||
|
{
|
||||||
|
label: t('FlowCanvas.toRealVariantsShortcut1Label'),
|
||||||
|
value: t('FlowCanvas.toRealVariantsShortcut1Value')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('FlowCanvas.toRealVariantsShortcut2Label'),
|
||||||
|
value: t('FlowCanvas.toRealVariantsShortcut2Value')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('FlowCanvas.toRealVariantsShortcut3Label'),
|
||||||
|
value: t('FlowCanvas.toRealVariantsShortcut3Value')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('FlowCanvas.toRealVariantsShortcut4Label'),
|
||||||
|
value: t('FlowCanvas.toRealVariantsShortcut4Value')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('FlowCanvas.toRealVariantsShortcut5Label'),
|
||||||
|
value: t('FlowCanvas.toRealVariantsShortcut5Value')
|
||||||
|
}
|
||||||
|
])
|
||||||
|
const stateManager = inject('stateManager') as any
|
||||||
|
const attrs = useAttrs()
|
||||||
|
const modeList = ref([
|
||||||
|
{ value: 'Advanced', label: 'Advanced' },
|
||||||
|
{ value: 'Normal', label: 'Normal' }
|
||||||
|
])
|
||||||
|
const data = reactive({
|
||||||
|
prompt: '',
|
||||||
|
pixelRatio: '1:1',
|
||||||
|
mode: 'Advanced',
|
||||||
|
})
|
||||||
|
|
||||||
|
const getApiData = ()=>{
|
||||||
|
let superior = stateManager.getNodeById(attrs.node?.data?.superiorID)
|
||||||
|
// let {superiorGenerateImg} = attrs
|
||||||
|
return {
|
||||||
|
mode: data.mode,
|
||||||
|
size: data.pixelRatio,
|
||||||
|
userPrompt: data.prompt,
|
||||||
|
original3dUrl: superior?.data?.superiorGenerateImg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ data, getApiData })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.to-real-style {
|
||||||
|
> .shortcut-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 2px;
|
||||||
|
user-select: none;
|
||||||
|
> .item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px 3px;
|
||||||
|
font-family: Medium;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
border: 1px solid #e4e4e7;
|
||||||
|
background: #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -17,9 +17,9 @@
|
|||||||
<span class="icon" @click="onDownload(item)">
|
<span class="icon" @click="onDownload(item)">
|
||||||
<svg-icon name="download" size="20" size-unit="px" />
|
<svg-icon name="download" size="20" size-unit="px" />
|
||||||
</span>
|
</span>
|
||||||
<button class="edit" @click="onEdit(item)" v-if="node.data.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL">
|
<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,8 +172,14 @@
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
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({glbPath:item?.glbPath,glbInfoObj:item?.glbInfoObj})
|
openThreeModelPreview({
|
||||||
|
glbPath:item?.glbPath,
|
||||||
|
glbInfoObj:item?.glbInfoObj,
|
||||||
|
nodeId:props.node?.id,
|
||||||
|
nodeType:props.node.data?.superiorNodeType,
|
||||||
|
superiorGenerateImg:props.node.data?.superiorGenerateImg,
|
||||||
|
})
|
||||||
}else{
|
}else{
|
||||||
openImagePreview(item.url)
|
openImagePreview(item.url)
|
||||||
}
|
}
|
||||||
@@ -232,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
|
||||||
}
|
}
|
||||||
@@ -379,6 +382,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
// height: 140px;
|
// height: 140px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 140px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -7,13 +7,18 @@ const props = defineProps({
|
|||||||
default: () => ({})
|
default: () => ({})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
//])
|
'captureView'
|
||||||
|
])
|
||||||
let data = reactive({
|
let data = reactive({
|
||||||
})
|
})
|
||||||
const onDownload = () => {
|
const onDownload = () => {
|
||||||
if(props?.config?.glbPath)downloadImage(props?.config?.glbPath, 'model.glb')
|
if(props?.config?.glbPath)downloadImage(props?.config?.glbPath, 'model.glb')
|
||||||
}
|
}
|
||||||
|
const captureView = ()=>{
|
||||||
|
emit('captureView')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
})
|
})
|
||||||
onUnmounted(()=>{
|
onUnmounted(()=>{
|
||||||
@@ -23,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>
|
||||||
@@ -52,32 +49,38 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
<div class="download" @click="onDownload">{{ $t('threeModel.download') }}</div>
|
<div class="captureView" @click="captureView">
|
||||||
|
<div class="icon">
|
||||||
|
<svgIcon name="captureView" size="12" />
|
||||||
|
</div>
|
||||||
|
{{ $t('threeModel.captureView') }}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="download" @click="onDownload">{{ $t('threeModel.download') }}</div> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.modalDetail{
|
.modalDetail{
|
||||||
width: 100%;
|
width: 22rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
> .title{
|
> .title{
|
||||||
margin: 2.4rem 0;
|
margin: 2.4rem 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -85,9 +88,9 @@ const {} = toRefs(data);
|
|||||||
line-height: 2.7rem;
|
line-height: 2.7rem;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
> .captureView ,
|
||||||
> .download{
|
> .download{
|
||||||
margin-left: 4.2rem;
|
transform: translateX(calc(13rem / 2));
|
||||||
margin-top: 24.8rem;
|
|
||||||
line-height: 3rem;
|
line-height: 3rem;
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
border-radius: 1.5rem;
|
border-radius: 1.5rem;
|
||||||
@@ -99,6 +102,15 @@ const {} = toRefs(data);
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
> .captureView{
|
||||||
|
margin-top: 6.4rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: .7rem;
|
||||||
|
}
|
||||||
|
> .download{
|
||||||
|
margin-top: 15.4rem;
|
||||||
|
}
|
||||||
> .detail{
|
> .detail{
|
||||||
> .name{
|
> .name{
|
||||||
> .title{
|
> .title{
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||||
import threeGlb from '@/assets/images/three/sample.glb'
|
import threeGlb from '@/assets/images/three/sample.glb'
|
||||||
|
import { uploadImage } from '@/api/upload'
|
||||||
|
import { base64Tofile } from '@/components/Canvas/tools/tools'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
import model from './model.vue'
|
import model from './model.vue'
|
||||||
import detail from './detail.vue'
|
import detail from './detail.vue'
|
||||||
@@ -11,12 +14,28 @@ const props = defineProps({
|
|||||||
default: () => ({})
|
default: () => ({})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
//const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
//])
|
'captureView'
|
||||||
|
])
|
||||||
let data = reactive({
|
let data = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
const modelRef = ref(null)
|
const modelRef = ref(null)
|
||||||
|
const captureView = async ()=>{
|
||||||
|
let url = modelRef.value.captureView()
|
||||||
|
|
||||||
|
const file = base64Tofile(url, 'canvas.png')
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
const minioUrl = await uploadImage(formData, true)
|
||||||
|
ElMessage.warning('Your new view has been captured.')
|
||||||
|
emit('captureView', {
|
||||||
|
minioUrl,
|
||||||
|
nodeId: props?.currentData?.nodeId,
|
||||||
|
nodeType: props?.currentData?.nodeType,
|
||||||
|
superiorGenerateImg: props?.currentData?.superiorGenerateImg,
|
||||||
|
})
|
||||||
|
}
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
// modelRef.value.open(threeGlb)
|
// modelRef.value.open(threeGlb)
|
||||||
if(props?.currentData?.glbPath)modelRef.value.open(props?.currentData?.glbPath)
|
if(props?.currentData?.glbPath)modelRef.value.open(props?.currentData?.glbPath)
|
||||||
@@ -32,7 +51,7 @@ const {} = toRefs(data);
|
|||||||
<model ref="modelRef" />
|
<model ref="modelRef" />
|
||||||
</div>
|
</div>
|
||||||
<div class="detailBox">
|
<div class="detailBox">
|
||||||
<detail ref="detailRef" :config="currentData" />
|
<detail ref="detailRef" @captureView="captureView" :config="currentData" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -49,8 +68,10 @@ const {} = toRefs(data);
|
|||||||
width: 65.5rem;
|
width: 65.5rem;
|
||||||
}
|
}
|
||||||
> .detailBox{
|
> .detailBox{
|
||||||
width: 22rem;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { initThree,clearModel,addModel,getModelInfo,calculateCameraPosition } from './threeTool'
|
import { ThreeManager } from './threeTool'
|
||||||
|
|
||||||
//const props = defineProps({
|
//const props = defineProps({
|
||||||
//})
|
//})
|
||||||
@@ -12,154 +12,40 @@ import { initThree,clearModel,addModel,getModelInfo,calculateCameraPosition } fr
|
|||||||
//])
|
//])
|
||||||
|
|
||||||
const threeDom = ref()//threeDom元素
|
const threeDom = ref()//threeDom元素
|
||||||
|
|
||||||
let scene = shallowRef()//场景
|
|
||||||
let group = shallowRef()//组
|
|
||||||
let camera = shallowRef()//相机
|
|
||||||
let renderer = shallowRef()//渲染器
|
|
||||||
let pointLight = shallowRef();//光
|
|
||||||
let controls = shallowRef()//监听鼠标、键盘事件
|
|
||||||
|
|
||||||
const animationId = ref(null);
|
|
||||||
//加载进度
|
//加载进度
|
||||||
const load = ref({
|
const load = ref({
|
||||||
state:false,
|
state:false,
|
||||||
progress:0
|
progress:0
|
||||||
})
|
})
|
||||||
// const textureLoader = ref(new THREE.TextureLoader())//材质
|
let threeModel = null
|
||||||
const init = async () => {
|
|
||||||
//初始化threejs
|
|
||||||
if (scene.value) return
|
|
||||||
const initResult = await initThree(threeDom.value)
|
|
||||||
scene.value = initResult.scene
|
|
||||||
group.value = initResult.group
|
|
||||||
camera.value = initResult.camera
|
|
||||||
renderer.value = initResult.renderer
|
|
||||||
pointLight.value = initResult.pointLight
|
|
||||||
controls.value = initResult.controls
|
|
||||||
// pointLight.value = initResult.pointLight
|
|
||||||
|
|
||||||
threeDom.value.ondblclick = (event:any)=>{
|
|
||||||
let intersects = openModel(event);
|
|
||||||
if(!intersects || intersects.length<=0) return
|
|
||||||
|
|
||||||
const clickedObject = intersects[0].object;
|
|
||||||
const modelInfo = getModelInfo(clickedObject);
|
|
||||||
const { size } = modelInfo;
|
|
||||||
const maxSize = Math.max(size.x, size.y, size.z);
|
|
||||||
let distanceFactor = 1.2;
|
|
||||||
let heightFactor = 0.3;
|
|
||||||
let angle = 0;
|
|
||||||
if (size.y > size.x * 2) {
|
|
||||||
// 高瘦物体,拉远一点,稍微抬高视角
|
|
||||||
distanceFactor = 1.5;
|
|
||||||
heightFactor = 0.4;
|
|
||||||
angle = Math.PI / 6; // 30度
|
|
||||||
} else if (size.x > size.y * 2) {
|
|
||||||
// 扁平物体,降低视角
|
|
||||||
heightFactor = 0.2;
|
|
||||||
angle = Math.PI / 8; // 22.5度
|
|
||||||
}
|
|
||||||
const { position: cameraPos, target } = calculateCameraPosition(
|
|
||||||
modelInfo,
|
|
||||||
camera.value,
|
|
||||||
{
|
|
||||||
distanceFactor,
|
|
||||||
heightFactor,
|
|
||||||
angle
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// 执行相机动画
|
|
||||||
animateCamera(
|
|
||||||
camera.value.position,
|
|
||||||
cameraPos,
|
|
||||||
controls.value.target,
|
|
||||||
target
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let openModel = (event:any)=>{
|
|
||||||
let mouse = new THREE.Vector2();
|
|
||||||
let raycaster = new THREE.Raycaster();
|
|
||||||
mouse.x=(event.clientX/window.innerWidth)*2-1;
|
|
||||||
mouse.y=-(event.clientY/window.innerHeight)*2+1;
|
|
||||||
raycaster.setFromCamera(mouse, camera.value);
|
|
||||||
let intersects = raycaster.intersectObjects(scene.value.children);
|
|
||||||
return intersects
|
|
||||||
}
|
|
||||||
let isTweening = false;
|
|
||||||
|
|
||||||
function animateCamera(
|
|
||||||
startCameraPos: THREE.Vector3,
|
|
||||||
endCameraPos: THREE.Vector3,
|
|
||||||
startTarget: THREE.Vector3,
|
|
||||||
endTarget: THREE.Vector3
|
|
||||||
) {
|
|
||||||
if (isTweening) return;
|
|
||||||
isTweening = true;
|
|
||||||
|
|
||||||
let options = {
|
|
||||||
cx: startCameraPos.x,
|
|
||||||
cy: startCameraPos.y,
|
|
||||||
cz: startCameraPos.z,
|
|
||||||
tx: startTarget.x,
|
|
||||||
ty: startTarget.y,
|
|
||||||
tz: startTarget.z
|
|
||||||
};
|
|
||||||
|
|
||||||
gsap.to(options, {
|
|
||||||
cx: endCameraPos.x,
|
|
||||||
cy: endCameraPos.y,
|
|
||||||
cz: endCameraPos.z,
|
|
||||||
tx: endTarget.x,
|
|
||||||
ty: endTarget.y,
|
|
||||||
tz: endTarget.z,
|
|
||||||
duration: 1,
|
|
||||||
ease: 'power2.inOut', // 使用更自然的缓动
|
|
||||||
onUpdate: () => {
|
|
||||||
camera.value.position.set(options.cx, options.cy, options.cz);
|
|
||||||
controls.value.target.set(options.tx, options.ty, options.tz);
|
|
||||||
controls.value.update();
|
|
||||||
},
|
|
||||||
onComplete: () => {
|
|
||||||
isTweening = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const setModel = async (url:any)=>{
|
const setModel = async (url:any)=>{
|
||||||
clearModel(group,scene)
|
await threeModel.setModel(url,load)
|
||||||
await addModel(url,controls,camera,pointLight,group,load)
|
|
||||||
// addMaterial()
|
|
||||||
}
|
}
|
||||||
const open = (url)=>{
|
const open = (url)=>{
|
||||||
load.value.state = true
|
load.value.state = true
|
||||||
nextTick(async ()=>{
|
nextTick(async ()=>{
|
||||||
await init()
|
threeModel = new ThreeManager(threeDom.value)
|
||||||
controls.value.enableDamping = true;
|
await threeModel.setHDRI()
|
||||||
let animate = ()=>{
|
|
||||||
animationId.value = requestAnimationFrame(animate);
|
|
||||||
// renderer.value.render(scene.value, camera.value);
|
|
||||||
// model.rotation.x += 0.01; //旋转物体
|
|
||||||
var vector = camera.value.position.clone()
|
|
||||||
controls.value.update();
|
|
||||||
renderer.value.render(scene.value, camera.value);
|
|
||||||
// point.position.set(vector.x,vector.y,vector.z);
|
|
||||||
// group.rotation.y += 0.01;
|
|
||||||
// composer.render();
|
|
||||||
};
|
|
||||||
animate();
|
|
||||||
await setModel(url)
|
await setModel(url)
|
||||||
load.value.state = false
|
load.value.state = false
|
||||||
|
threeModel.operation()
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const captureView = ()=>{
|
||||||
|
return threeModel.exportAsImage()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(()=>{
|
onMounted(()=>{
|
||||||
})
|
})
|
||||||
onUnmounted(()=>{
|
onUnmounted(()=>{
|
||||||
|
console.log('onUnmounted')
|
||||||
|
threeModel.disposeModel()
|
||||||
|
threeModel = null
|
||||||
})
|
})
|
||||||
defineExpose({open})
|
defineExpose({open,captureView})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="modelBox">
|
<div class="modelBox">
|
||||||
|
|||||||
@@ -3,258 +3,432 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|||||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
|
||||||
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'
|
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'
|
||||||
|
import { RectAreaLightUniformsLib } from 'three/examples/jsm/lights/RectAreaLightUniformsLib.js';
|
||||||
import hdri from '@/assets/images/three/hdri.hdr'
|
import hdri from '@/assets/images/three/hdri.hdr'
|
||||||
|
|
||||||
export const initThree = async (threeDom)=>{
|
interface ModelInfo {
|
||||||
const scene = new THREE.Scene();
|
box: THREE.Box3;
|
||||||
const group = new THREE.Group()
|
center: THREE.Vector3;
|
||||||
scene.add(group)
|
size: THREE.Vector3;
|
||||||
|
maxSize: number;
|
||||||
|
}
|
||||||
|
const CONFIG = {
|
||||||
|
hdriIntensity: 7.4,
|
||||||
|
exposureBase: 0.92,
|
||||||
|
backlightBoost: { min: 2.1, max: 4.8 }, // 背光增强系数
|
||||||
|
hdriUrl: hdri,
|
||||||
|
};
|
||||||
|
export class ThreeManager {
|
||||||
|
threeDom: HTMLElement;
|
||||||
|
scene: THREE.Scene;//场景对象
|
||||||
|
camera: THREE.PerspectiveCamera;//相机对象
|
||||||
|
renderer: THREE.WebGLRenderer;//渲染器对象
|
||||||
|
controls: OrbitControls;//轨道控制器对象
|
||||||
|
animationId: number | null = null;
|
||||||
|
|
||||||
//创建相机对象
|
pointLight: THREE.AmbientLight;//环境光对象
|
||||||
// this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
|
studioLights: any;//工作室光对象数组
|
||||||
const camera = new THREE.PerspectiveCamera(45, threeDom.offsetWidth / threeDom.offsetHeight, 0.1, 10000);
|
v1: THREE.Vector3;//相机前向向量
|
||||||
camera.position.set(0, 90, 6); //设置相机位置
|
camDir: THREE.Vector3;//相机前向向量
|
||||||
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
|
camForward: THREE.Vector3;//相机前向向量
|
||||||
/**
|
camToTarget: THREE.Vector3;//相机目标向量
|
||||||
* 创建渲染器对象
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
currentModel: any;//当前模型对象
|
||||||
|
animate: any;//动画对象
|
||||||
|
|
||||||
const width = threeDom.offsetWidth; //窗口宽度
|
modelInfo: ModelInfo;//模型信息对象
|
||||||
const height = threeDom.offsetHeight; //窗口高度
|
defaultSoftboxPositions: Array<{ x: number; y: number; z: number; intensity: number; w: number; h: number }> = [
|
||||||
const renderer = new THREE.WebGLRenderer({
|
{ x: 0, y: 5.8, z: 3.5, intensity: 3.5, w: 6, h: 4.8 }, // 主光
|
||||||
antialias: true,
|
{ x: 0, y: 2.8, z: 7.5, intensity: 2.2, w: 5, h: 4 }, // 前光
|
||||||
logarithmicDepthBuffer: true,//深度缓存 防止模型闪烁重影
|
{ x: 0, y: 2.8, z: -7.5, intensity: 6.5, w: 5, h: 4 }, // 背光
|
||||||
});
|
{ x: -7.5, y: 2.8, z: 0, intensity: 10.8, w: 5, h: 4 }, // 侧光
|
||||||
|
{ x: 7.5, y: 2.8, z: 0, intensity: 2.0, w: 5, h: 4 }, // 侧光
|
||||||
|
{ x: 0, y: -2.2, z: 3, intensity: 0.8, w: 9, h: 4 } // 地面反光板
|
||||||
|
];
|
||||||
|
constructor(threeDom: HTMLElement,config:any = {}){
|
||||||
|
|
||||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;//设置色调
|
this.threeDom = threeDom;
|
||||||
renderer.toneMappingExposure = 1.3;
|
this.studioLights = []
|
||||||
|
//创建场景
|
||||||
|
this.scene = new THREE.Scene();
|
||||||
|
this.scene.background = new THREE.Color(0xffffff);
|
||||||
|
//创建相机
|
||||||
|
this.camera = new THREE.PerspectiveCamera(45, threeDom.offsetWidth / threeDom.offsetHeight, 0.1, 10000);
|
||||||
|
this.camera.position.set(0, 1.5, 6); //设置相机位置
|
||||||
|
|
||||||
renderer.shadowMap.enabled = true;
|
this.v1 = new THREE.Vector3();
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
this.camDir = new THREE.Vector3();
|
||||||
renderer.setSize(width, height); //设置渲染区域尺寸
|
this.camForward = new THREE.Vector3();
|
||||||
renderer.setClearColor(0xffffff, 1); //设置背景颜色
|
this.camToTarget = new THREE.Vector3();
|
||||||
threeDom.innerHTML = '';
|
//设置渲染器
|
||||||
threeDom.appendChild(renderer.domElement);
|
this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance", preserveDrawingBuffer: true });
|
||||||
|
//设置环境光
|
||||||
|
this.scene.add(new THREE.AmbientLight(0xffffff, 0.15));
|
||||||
|
|
||||||
// 设置渲染器大小
|
// 关键优化:物理光照与色彩管理
|
||||||
|
this.renderer.outputEncoding = THREE.sRGBEncoding;
|
||||||
|
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
this.renderer.toneMappingExposure = CONFIG.exposureBase;
|
||||||
|
this.renderer.physicallyCorrectLights = true; // 开启物理光照模式,光衰减更真实
|
||||||
|
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||||
|
this.renderer.setSize(threeDom.offsetWidth, threeDom.offsetHeight); //设置渲染区域尺寸
|
||||||
|
this.renderer.setClearColor(0xffffff, 1); //设置背景颜色
|
||||||
|
threeDom.innerHTML = '';
|
||||||
|
threeDom.appendChild(this.renderer.domElement);
|
||||||
|
|
||||||
|
RectAreaLightUniformsLib.init();
|
||||||
|
//设置轨道控制器
|
||||||
|
this.controls = new OrbitControls(this.camera,this.renderer.domElement)//监听鼠标、键盘事件;
|
||||||
|
// controls.minDistance = 500; // 设置相机与焦点的最小距离
|
||||||
|
// controls.maxDistance = 4000; // 设置相机与焦点的最大距离
|
||||||
|
this.controls.mouseButtons = {
|
||||||
|
// LEFT:THREE.MOUSE.PAN, // 左键 拖动(默认旋转:ROTATE)
|
||||||
|
LEFT:THREE.MOUSE.ROTATE, // 左键 拖动(默认旋转:ROTATE)
|
||||||
|
MIDDLE:THREE.MOUSE.DOLLY, // 滑轮 缩放
|
||||||
|
RIGHT:THREE.MOUSE.PAN // 右键 旋转(默认拖动:PAN)
|
||||||
|
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转(默认拖动:PAN)
|
||||||
|
}
|
||||||
|
this.controls.enableDamping = true;
|
||||||
|
|
||||||
const controls = new OrbitControls(camera,renderer.domElement)//监听鼠标、键盘事件;
|
|
||||||
// controls.minDistance = 500; // 设置相机与焦点的最小距离
|
|
||||||
// controls.maxDistance = 4000; // 设置相机与焦点的最大距离
|
|
||||||
controls.mouseButtons = {
|
|
||||||
// LEFT:THREE.MOUSE.PAN, // 左键 拖动(默认旋转:ROTATE)
|
|
||||||
LEFT:THREE.MOUSE.ROTATE, // 左键 拖动(默认旋转:ROTATE)
|
|
||||||
MIDDLE:THREE.MOUSE.DOLLY, // 滑轮 缩放
|
|
||||||
RIGHT:THREE.MOUSE.PAN // 右键 旋转(默认拖动:PAN)
|
|
||||||
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转(默认拖动:PAN)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const rgbeLoader = new RGBELoader()
|
|
||||||
const hdrTexture = await rgbeLoader.loadAsync(hdri)
|
|
||||||
hdrTexture.mapping = THREE.EquirectangularMapping
|
|
||||||
|
|
||||||
// 设置环境贴图(影响材质反射)
|
|
||||||
scene.environment = hdrTexture
|
|
||||||
// 可选:同时设置为背景
|
|
||||||
scene.background = hdrTexture
|
|
||||||
|
|
||||||
console.log('HDR 环境贴图加载成功:', hdri)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('HDR 加载失败:', error)
|
|
||||||
// 降级方案:使用环境光
|
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
|
|
||||||
scene.add(ambientLight)
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 光源设置
|
* 根据模型大小计算自适应配置
|
||||||
*/
|
*/
|
||||||
//点光源
|
calculateAdaptiveConfig(modelInfo: ModelInfo) {
|
||||||
/**
|
const { size, maxSize, center } = modelInfo;
|
||||||
* AmbientLight 环境光
|
// 基础距离系数(可根据需要调整)
|
||||||
PointLight 点光源
|
let distanceFactor = 1.5;
|
||||||
DirectionalLight 平行光,比如太阳光
|
// 根据模型形状调整距离系数
|
||||||
SpotLight 聚光源
|
const aspectRatio = size.x / size.y;
|
||||||
*/
|
if (aspectRatio > 2) {
|
||||||
|
// 扁平模型,拉远一点
|
||||||
// 2. 定向光(主光源)
|
distanceFactor = 3.0;
|
||||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
} else if (aspectRatio < 0.5) {
|
||||||
// 设置定向光的目标点(指向原点)
|
// 高瘦模型,稍微拉近
|
||||||
directionalLight.target.position.set(0, 0, 0);
|
distanceFactor = 2.0;
|
||||||
scene.add(directionalLight);
|
}
|
||||||
|
// 计算相机距离
|
||||||
//设置环境光全亮
|
const fov = this.camera.fov * (Math.PI / 180);
|
||||||
//环境光
|
const cameraDistance = (maxSize / 2) / Math.tan(fov / 2) * distanceFactor;
|
||||||
const pointLight = new THREE.AmbientLight(0xffffff,.8);
|
// 灯光缩放系数
|
||||||
scene.add(pointLight);
|
const lightScale = Math.max(0.5, Math.min(2.0, maxSize / 2.5));
|
||||||
// const pointLight = new THREE.AmbientLight(0xffffff,1.0);
|
// 强度缩放系数(模型越大,灯光需要越强)
|
||||||
// pointLight.intensity = 1.2//光源强度
|
const intensityScale = Math.max(0.6, Math.min(2.5, maxSize / 2));
|
||||||
// pointLight.castShadow = true//开启阴影
|
|
||||||
// pointLight.shadow.mapSize = new THREE.Vector2(width, height)
|
|
||||||
// scene.add(pointLight); //点光源添加到场景中
|
|
||||||
// pointLight.position.set(400, 200, 300); //点光源位置
|
|
||||||
// pointLight.position.y = 100;
|
|
||||||
// pointLight.position.z = 50;
|
|
||||||
// pointLight.position.x = 100;
|
|
||||||
|
|
||||||
// let floorGeometry = new THREE.PlaneGeometry(5000, 3000)//地板大小
|
|
||||||
// let floorMaterial = new THREE.MeshPhongMaterial({ color: "#7e7ab0", })
|
|
||||||
// let floorMesh = new THREE.Mesh(floorGeometry, floorMaterial);
|
|
||||||
// floorMesh.rotation.x = -0.5 * Math.PI;
|
|
||||||
// floorMesh.receiveShadow = true;
|
|
||||||
// floorMesh.position.y = -0.001;
|
|
||||||
// scene.add(floorMesh);
|
|
||||||
const textureLoader = new THREE.TextureLoader();
|
|
||||||
// const texture = textureLoader.load('/3dModel/sketch-thick.jpg');
|
|
||||||
scene.background = new THREE.Color("#fff");
|
|
||||||
return {scene,group,camera,renderer,controls,pointLight}
|
|
||||||
}
|
|
||||||
export const clearModel = (group,scene)=>{
|
|
||||||
const oldGroup:any = group.value;
|
|
||||||
group.value = new THREE.Group();
|
|
||||||
scene.value.add(group.value);
|
|
||||||
scene.value.remove(oldGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算模型包围盒
|
|
||||||
export const getModelInfo = (model: THREE.Object3D) => {
|
|
||||||
const box = new THREE.Box3().setFromObject(model);
|
|
||||||
const center = box.getCenter(new THREE.Vector3());
|
|
||||||
const size = box.getSize(new THREE.Vector3());
|
|
||||||
const maxSize = Math.max(size.x, size.y, size.z);
|
|
||||||
return {
|
|
||||||
box,
|
|
||||||
center,
|
|
||||||
size,
|
|
||||||
maxSize
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// 根据模型信息计算相机位置
|
|
||||||
export const calculateCameraPosition = (
|
|
||||||
modelInfo: ReturnType<typeof getModelInfo>,
|
|
||||||
camera: THREE.PerspectiveCamera,
|
|
||||||
options?: {
|
|
||||||
distanceFactor?: number; // 距离系数,默认1.5
|
|
||||||
heightFactor?: number; // 高度偏移系数,默认0.3
|
|
||||||
angle?: number; // 观察角度,默认正前方
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const { center, size, maxSize } = modelInfo;
|
|
||||||
const fov = camera.fov * (Math.PI / 180);
|
|
||||||
|
|
||||||
const distanceFactor = options?.distanceFactor ?? 1.5;
|
|
||||||
const heightFactor = options?.heightFactor ?? 0.3;
|
|
||||||
|
|
||||||
// 计算合适的相机距离
|
|
||||||
const distance = (maxSize / 2) / Math.tan(fov / 2) * distanceFactor;
|
|
||||||
|
|
||||||
// 根据角度计算相机位置
|
|
||||||
const angle = options?.angle ?? 0; // 0表示正前方
|
|
||||||
|
|
||||||
return {
|
|
||||||
position: new THREE.Vector3(
|
|
||||||
center.x + distance * Math.sin(angle),
|
|
||||||
center.y + size.y * heightFactor,
|
|
||||||
center.z + distance * Math.cos(angle)
|
|
||||||
),
|
|
||||||
target: center.clone(),
|
|
||||||
distance,
|
|
||||||
center,
|
|
||||||
size
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// 根据模型信息计算光源位置
|
|
||||||
export const calculateLightPosition = (
|
|
||||||
modelInfo: ReturnType<typeof getModelInfo>,
|
|
||||||
options?: {
|
|
||||||
xFactor?: number; // X轴偏移系数,默认0.5
|
|
||||||
yFactor?: number; // Y轴偏移系数,默认0.8
|
|
||||||
zFactor?: number; // Z轴偏移系数,默认0.5
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const { center, size } = modelInfo;
|
|
||||||
|
|
||||||
const xFactor = options?.xFactor ?? 0.5;
|
|
||||||
const yFactor = options?.yFactor ?? 0.8;
|
|
||||||
const zFactor = options?.zFactor ?? 0.5;
|
|
||||||
|
|
||||||
return new THREE.Vector3(
|
|
||||||
center.x + size.x * xFactor,
|
|
||||||
center.y + size.y * yFactor,
|
|
||||||
center.z + size.z * zFactor
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export const addModel = async (
|
|
||||||
url: any,
|
|
||||||
controls: OrbitControls,
|
|
||||||
camera: THREE.PerspectiveCamera,
|
|
||||||
pointLight: THREE.DirectionalLight,
|
|
||||||
group: THREE.Group,
|
|
||||||
load: any
|
|
||||||
) => {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const fbxLoader = new GLTFLoader();
|
|
||||||
const drac = new DRACOLoader()
|
|
||||||
drac.setDecoderPath('/draco/')
|
|
||||||
fbxLoader.setDRACOLoader(drac)
|
|
||||||
|
|
||||||
fbxLoader.load(url,
|
console.log('自适应配置:', {
|
||||||
(obj: any) => {
|
相机距离: cameraDistance,
|
||||||
const scene = obj.scene;
|
灯光缩放: lightScale,
|
||||||
scene.traverse((child: any) => {
|
强度缩放: intensityScale,
|
||||||
if (child.isMesh) {
|
目标中心: center
|
||||||
// 如果是基础材质,转换为标准材质
|
});
|
||||||
if (child.material instanceof THREE.MeshBasicMaterial) {
|
|
||||||
const oldMat = child.material;
|
return {
|
||||||
child.material = new THREE.MeshStandardMaterial({
|
cameraDistance,
|
||||||
map: oldMat.map,
|
lightScale,
|
||||||
color: oldMat.color,
|
intensityScale,
|
||||||
roughness: 0.4,
|
targetCenter: center.clone()
|
||||||
metalness: 0
|
};
|
||||||
});
|
}
|
||||||
}
|
/**
|
||||||
// 如果是标准材质,调整粗糙度
|
* 获取模型信息(包围盒、中心点、尺寸等)
|
||||||
else if (child.material instanceof THREE.MeshStandardMaterial) {
|
*/
|
||||||
child.material.roughness = 0.4;
|
getModelInfo(model: THREE.Object3D): ModelInfo {
|
||||||
child.material.metalness = 0;
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const maxSize = Math.max(size.x, size.y, size.z);
|
||||||
|
|
||||||
|
console.log('模型信息:', {
|
||||||
|
中心点: center,
|
||||||
|
尺寸: { x: size.x, y: size.y, z: size.z },
|
||||||
|
最大尺寸: maxSize
|
||||||
|
});
|
||||||
|
|
||||||
|
return { box, center, size, maxSize };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 根据模型大小自适应创建柔光箱灯光
|
||||||
|
*/
|
||||||
|
createAdaptiveSoftboxes(modelInfo: ModelInfo) {
|
||||||
|
const { lightScale, intensityScale, targetCenter } = this.calculateAdaptiveConfig(modelInfo);
|
||||||
|
// 清除现有灯光
|
||||||
|
this.studioLights.forEach(item => {
|
||||||
|
this.scene.remove(item.light);
|
||||||
|
});
|
||||||
|
this.studioLights = [];
|
||||||
|
|
||||||
|
// 根据模型大小创建自适应灯光
|
||||||
|
this.defaultSoftboxPositions.forEach(pos => {
|
||||||
|
const scaledX = pos.x * lightScale;
|
||||||
|
const scaledY = pos.y * lightScale;
|
||||||
|
const scaledZ = pos.z * lightScale;
|
||||||
|
const scaledW = pos.w * lightScale;
|
||||||
|
const scaledH = pos.h * lightScale;
|
||||||
|
const scaledIntensity = pos.intensity * intensityScale;
|
||||||
|
|
||||||
|
const light = new THREE.RectAreaLight(0xffffff, scaledIntensity, scaledW, scaledH);
|
||||||
|
light.position.set(
|
||||||
|
targetCenter.x + scaledX,
|
||||||
|
targetCenter.y + scaledY,
|
||||||
|
targetCenter.z + scaledZ
|
||||||
|
);
|
||||||
|
light.lookAt(targetCenter);
|
||||||
|
this.scene.add(light);
|
||||||
|
|
||||||
|
this.studioLights.push({
|
||||||
|
light,
|
||||||
|
offset: new THREE.Vector3(scaledX, scaledY, scaledZ),
|
||||||
|
baseIntensity: scaledIntensity
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log('自适应灯光已创建,缩放系数:', lightScale, '强度系数:', intensityScale);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 根据模型大小调整相机位置
|
||||||
|
*/
|
||||||
|
adjustCameraToModel(modelInfo: ModelInfo) {
|
||||||
|
const { cameraDistance, targetCenter } = this.calculateAdaptiveConfig(modelInfo);
|
||||||
|
// 正面俯视角度:相机在模型正前方,稍高于模型中心
|
||||||
|
// X 轴:模型中心(正面视角,不左右偏移)
|
||||||
|
// Y 轴:相机高度 = 模型中心高度 + 模型高度的 0.3 倍(俯视效果)
|
||||||
|
// Z 轴:相机距离 = 根据模型大小计算的距离
|
||||||
|
const x = targetCenter.x;
|
||||||
|
const y = targetCenter.y + modelInfo.size.y * 0.3; // 相机高度为模型中心的 0.3 倍
|
||||||
|
const z = targetCenter.z + cameraDistance;
|
||||||
|
|
||||||
|
this.camera.position.set(x, y, z);
|
||||||
|
this.controls.target.copy(targetCenter);
|
||||||
|
this.controls.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
createSoftbox(x, y, z, intensity, w = 5, h = 4) {
|
||||||
|
const light = new THREE.RectAreaLight(0xffffff, intensity, w, h);
|
||||||
|
light.position.set(x, y, z);
|
||||||
|
light.lookAt(0, 0, 0);
|
||||||
|
this.scene.add(light);
|
||||||
|
// 存储 offset 向量副本,避免后续重复创建
|
||||||
|
this.studioLights.push({
|
||||||
|
light,
|
||||||
|
offset: new THREE.Vector3(x, y, z),
|
||||||
|
baseIntensity: intensity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async setHDRI(){
|
||||||
|
const rgbeLoader = new RGBELoader()
|
||||||
|
await rgbeLoader.load(CONFIG.hdriUrl, (texture)=>{
|
||||||
|
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||||
|
const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
|
||||||
|
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
|
||||||
|
|
||||||
|
this.scene.environment = envMap;
|
||||||
|
this.scene.environmentIntensity = CONFIG.hdriIntensity;
|
||||||
|
|
||||||
|
pmremGenerator.dispose();
|
||||||
|
texture.dispose();
|
||||||
|
console.log('✅ HDRI Loaded');
|
||||||
|
});
|
||||||
|
this.createSoftbox(0, 5.8, 3.5, 3.5, 6, 4.8); // 主光
|
||||||
|
this.createSoftbox(0, 2.8, 7.5, 2.2); // 前光
|
||||||
|
this.createSoftbox(0, 2.8, -7.5, 6.5); // 背光 (增强)
|
||||||
|
this.createSoftbox(-7.5, 2.8, 0, 10.8); // 侧光
|
||||||
|
this.createSoftbox(7.5, 2.8, 0, 2.0);
|
||||||
|
this.createSoftbox(0, -2.2, 3, 0.8, 9, 4); // 地面反光板
|
||||||
|
}
|
||||||
|
// 更新工作室光位置
|
||||||
|
updateStudioLighting(){
|
||||||
|
this.camera.getWorldDirection(this.camDir);
|
||||||
|
this.studioLights.forEach(item => {
|
||||||
|
// 使用 applyQuaternion 同步灯光位置
|
||||||
|
this.v1.copy(item.offset).applyQuaternion(this.camera.quaternion);
|
||||||
|
item.light.position.copy(this.controls.target).add(this.v1);
|
||||||
|
item.light.lookAt(this.controls.target);
|
||||||
|
|
||||||
|
// 动态背光逻辑
|
||||||
|
if (item.offset.z < -2) {
|
||||||
|
this.v1.copy(item.light.position).sub(this.controls.target).normalize();
|
||||||
|
const dot = this.v1.dot(this.camDir);
|
||||||
|
const factor = THREE.MathUtils.lerp(CONFIG.backlightBoost.min, CONFIG.backlightBoost.max, Math.max(0, 1 - dot));
|
||||||
|
item.light.intensity = item.baseIntensity * factor;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//动态设置曝光
|
||||||
|
updateImagePipeline() {
|
||||||
|
let exposure = 1.0;
|
||||||
|
this.camera.getWorldDirection(this.camForward);
|
||||||
|
this.camToTarget.copy(this.controls.target).sub(this.camera.position).normalize();
|
||||||
|
const facing = Math.abs(this.camForward.dot(this.camToTarget));
|
||||||
|
const targetExposure = THREE.MathUtils.lerp(1.25, 0.90, facing);
|
||||||
|
exposure = THREE.MathUtils.lerp(exposure, targetExposure, 0.08);
|
||||||
|
this.renderer.toneMappingExposure = CONFIG.exposureBase * exposure;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setModel(modelUrl,load){
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const drac = new DRACOLoader()
|
||||||
|
drac.setDecoderPath('/draco/')
|
||||||
|
const loader = new GLTFLoader().setDRACOLoader(drac);
|
||||||
|
|
||||||
|
if (this.currentModel) {
|
||||||
|
this.scene.remove(this.currentModel);
|
||||||
|
this.dispose(this.currentModel); // 核心优化:释放显存
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.load(modelUrl,
|
||||||
|
(gltf) => {
|
||||||
|
this.currentModel = gltf.scene;
|
||||||
|
|
||||||
|
// 遍历模型:增强材质表现
|
||||||
|
this.currentModel.traverse(child => {
|
||||||
|
if (child.isMesh) {
|
||||||
|
child.castShadow = true;
|
||||||
|
child.receiveShadow = true;
|
||||||
|
if (child.material) {
|
||||||
|
child.material.envMapIntensity = 1.2; // 增强 HDRI 反射强度
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const box = new THREE.Box3().setFromObject(this.currentModel);
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
// this.currentModel.position.sub(center);
|
||||||
|
this.scene.add(this.currentModel);
|
||||||
|
this.modelInfo = this.getModelInfo(this.currentModel)
|
||||||
|
// 根据模型大小调整相机位置
|
||||||
|
this.adjustCameraToModel(this.modelInfo);
|
||||||
|
|
||||||
|
// 根据模型大小创建自适应灯光
|
||||||
|
this.createAdaptiveSoftboxes(this.modelInfo);
|
||||||
|
resolve('')
|
||||||
|
},
|
||||||
|
(xhr: any) => { // 加载进度回调
|
||||||
|
const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2);
|
||||||
|
load.value.progress = percent
|
||||||
|
console.log('模型加载进度:', percent);
|
||||||
|
},
|
||||||
|
(error: any) => { // 加载失败回调
|
||||||
|
console.error('模型加载失败:', error);
|
||||||
|
resolve('')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
operation(){
|
||||||
|
const animate = () => {
|
||||||
|
this.animationId = requestAnimationFrame(animate);
|
||||||
|
this.controls?.update();
|
||||||
|
this.updateStudioLighting();
|
||||||
|
this.updateImagePipeline();
|
||||||
|
this.renderer?.render(this.scene, this.camera);
|
||||||
|
};
|
||||||
|
animate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 释放模型资源
|
||||||
|
dispose(obj){
|
||||||
|
if (!obj) return;
|
||||||
|
obj.traverse(node => {
|
||||||
|
if (node.isMesh) {
|
||||||
|
if (node.geometry) node.geometry.dispose();
|
||||||
|
if (node.material) {
|
||||||
|
if (Array.isArray(node.material)) {
|
||||||
|
node.material.forEach(m => m.dispose());
|
||||||
|
} else {
|
||||||
|
node.material.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
// 获取模型信息
|
});
|
||||||
const modelInfo = getModelInfo(scene);
|
|
||||||
const { position: cameraPos, target } = calculateCameraPosition(
|
while (this.scene.children.length) {
|
||||||
modelInfo,
|
this.scene.remove(this.scene.children[0]);
|
||||||
camera.value,
|
|
||||||
{
|
|
||||||
distanceFactor: 1.5,
|
|
||||||
heightFactor: 0.3,
|
|
||||||
angle: 0 // 正前方
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// 设置相机位置
|
|
||||||
camera.value.position.copy(cameraPos);
|
|
||||||
// 设置控制器目标点
|
|
||||||
controls.value.target.copy(target);
|
|
||||||
// 计算并设置光源位置
|
|
||||||
const lightPos = calculateLightPosition(modelInfo);
|
|
||||||
pointLight.value.position.copy(lightPos);
|
|
||||||
// 将模型添加到场景
|
|
||||||
group.value.add(scene);
|
|
||||||
// 可选:将模型信息存储在模型上,方便后续使用
|
|
||||||
(scene as any).userData.modelInfo = modelInfo;
|
|
||||||
|
|
||||||
resolve('')
|
|
||||||
},
|
|
||||||
(xhr: any) => { // 加载进度回调
|
|
||||||
const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2);
|
|
||||||
load.value.progress = percent
|
|
||||||
console.log('模型加载进度:', percent);
|
|
||||||
},
|
|
||||||
(error: any) => { // 加载失败回调
|
|
||||||
console.error('模型加载失败:', error);
|
|
||||||
reject('')
|
|
||||||
}
|
}
|
||||||
)
|
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(){
|
||||||
|
return this.renderer.domElement.toDataURL('image/png');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<image-preview ref="imagePreviewRef" />
|
<image-preview ref="imagePreviewRef" />
|
||||||
<baseModal ref="threeModelRef">
|
<baseModal ref="threeModelRef">
|
||||||
<template v-slot="{ currentData }">
|
<template v-slot="{ currentData }">
|
||||||
<threeModel :currentData="currentData" />
|
<threeModel :currentData="currentData" @captureView="captureView" />
|
||||||
</template>
|
</template>
|
||||||
</baseModal>
|
</baseModal>
|
||||||
<Assistant />
|
<Assistant />
|
||||||
@@ -253,6 +253,29 @@
|
|||||||
const openThreeModelPreview = (currentData) => {
|
const openThreeModelPreview = (currentData) => {
|
||||||
threeModelRef.value.open(currentData)
|
threeModelRef.value.open(currentData)
|
||||||
}
|
}
|
||||||
|
const captureView = (captureData)=>{
|
||||||
|
const timestamp = Date.now()
|
||||||
|
nodeManager.createResultNode({
|
||||||
|
data: {
|
||||||
|
superiorID_: captureData.nodeId,
|
||||||
|
superiorNodeType: captureData.nodeType,
|
||||||
|
superiorGenerateImg: captureData.superiorGenerateImg,
|
||||||
|
data: {
|
||||||
|
selectable: false,
|
||||||
|
imageProcessTasks: [
|
||||||
|
{
|
||||||
|
id: timestamp + '',
|
||||||
|
url: captureData.minioUrl,
|
||||||
|
status: 'RETURNED',
|
||||||
|
taskId: timestamp + ''
|
||||||
|
}
|
||||||
|
],
|
||||||
|
selectTaskId: timestamp + ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// threeModelRef.value.close()
|
||||||
|
}
|
||||||
provide('openImagePreview', openImagePreview)
|
provide('openImagePreview', openImagePreview)
|
||||||
provide('openThreeModelPreview', openThreeModelPreview)
|
provide('openThreeModelPreview', openThreeModelPreview)
|
||||||
|
|
||||||
@@ -266,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: {
|
||||||
@@ -284,6 +308,9 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const putFlowJson = async () => {
|
||||||
|
await stateManager.exportFlow(0,true)
|
||||||
|
}
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stateManager.dispose()
|
stateManager.dispose()
|
||||||
eventManager.dispose()
|
eventManager.dispose()
|
||||||
@@ -292,7 +319,8 @@
|
|||||||
toolManager.dispose()
|
toolManager.dispose()
|
||||||
})
|
})
|
||||||
defineExpose({
|
defineExpose({
|
||||||
getFlowJson
|
getFlowJson,
|
||||||
|
putFlowJson
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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() },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ interface NodeData {
|
|||||||
data?: object// 节点数据
|
data?: object// 节点数据
|
||||||
tier?: string// 节点层级
|
tier?: string// 节点层级
|
||||||
isHeader?: boolean// 是否显示头
|
isHeader?: boolean// 是否显示头
|
||||||
superiorID?: string// 上级节点ID
|
superiorID?: string// 上级节点ID,有连接线
|
||||||
|
superiorID_?: string// 上级节点ID,没有连接线
|
||||||
superiorNodeType?: string// 上级节点类型
|
superiorNodeType?: string// 上级节点类型
|
||||||
|
superiorGenerateImg?: string// 上级生成节点图片
|
||||||
disableDelete?: boolean// 是否禁用删除
|
disableDelete?: boolean// 是否禁用删除
|
||||||
disableCopy?: boolean// 是否禁用复制
|
disableCopy?: boolean// 是否禁用复制
|
||||||
createIndexPosition?: number// 创建索引位置
|
createIndexPosition?: number// 创建索引位置
|
||||||
isActive?: boolean// 是否激活
|
isActive?: boolean// 是否激活
|
||||||
|
versionImgUpdataList?: Array<any>// 版本节点
|
||||||
}
|
}
|
||||||
interface NodeOptions {
|
interface NodeOptions {
|
||||||
id?: string
|
id?: string
|
||||||
@@ -45,23 +48,31 @@ export class NodeManager {
|
|||||||
/** 创建节点 */
|
/** 创建节点 */
|
||||||
createNode(options: NodeOptions) {
|
createNode(options: NodeOptions) {
|
||||||
const superiorID = options?.data?.superiorID
|
const superiorID = options?.data?.superiorID
|
||||||
|
const superiorID_ = options?.data?.superiorID_// 上级节点ID,用于创建子节点时使用
|
||||||
//获取上级节点所生成的最后一个node,设置位置为最后一个节点的xy 加上 节点间距
|
//获取上级节点所生成的最后一个node,设置位置为最后一个节点的xy 加上 节点间距
|
||||||
const superiorGenerateNodes = this.stateManager.getSubordNodes(superiorID)
|
const superiorGenerateNodes = superiorID?this.stateManager.getSubordNodes(superiorID):[]
|
||||||
const currentNode = superiorGenerateNodes.find((node) => {
|
const currentNode = superiorGenerateNodes.find((node) => {
|
||||||
return (node.data.createIndexPosition === options?.data?.createIndexPosition && options?.data?.createIndexPosition)
|
return (node.data.createIndexPosition === options?.data?.createIndexPosition && options?.data?.createIndexPosition)
|
||||||
})
|
})
|
||||||
const endGenerateNode = superiorGenerateNodes.reduce((max, current) => {
|
const endGenerateNode = superiorGenerateNodes.reduce((max, current) => {
|
||||||
return current.data.createIndexPosition > max.data.createIndexPosition ? current : max
|
return current.data.createIndexPosition > max.data.createIndexPosition ? current : max
|
||||||
}, superiorGenerateNodes[0])
|
}, superiorGenerateNodes[0])
|
||||||
const snode = superiorID ? this.stateManager.flowManager.getNodeById(superiorID) : this.stateManager.flowManager.getLastNode();
|
let snode = null as any
|
||||||
|
if(superiorID){
|
||||||
|
snode = this.stateManager.flowManager.getNodeById(superiorID)
|
||||||
|
}else if(superiorID_){
|
||||||
|
snode = this.stateManager.flowManager.getNodeById(superiorID_)
|
||||||
|
}else{
|
||||||
|
snode = this.stateManager.flowManager.getLastNode()
|
||||||
|
}
|
||||||
const id = options.id || createId()
|
const id = options.id || createId()
|
||||||
const positionX = options.positionX || 0
|
const positionX = options.positionX || 0
|
||||||
const positionY = options.positionY || 0
|
const positionY = options.positionY || 0
|
||||||
const position = options.position ||
|
const position = options.position ||
|
||||||
(
|
(
|
||||||
currentNode ?
|
currentNode ?//当前节点位置,覆盖操作
|
||||||
currentNode.position :
|
currentNode.position :
|
||||||
endGenerateNode ?
|
endGenerateNode ?//最大子级位置
|
||||||
{
|
{
|
||||||
x: endGenerateNode.position.x + positionX,
|
x: endGenerateNode.position.x + positionX,
|
||||||
y: endGenerateNode.position.y + positionY + this.ranksep + 200
|
y: endGenerateNode.position.y + positionY + this.ranksep + 200
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ export const NODE_DATATYPE = {
|
|||||||
RESULT_IMAGE: 'result-image',
|
RESULT_IMAGE: 'result-image',
|
||||||
CARDS_SELECT: 'cards-select',
|
CARDS_SELECT: 'cards-select',
|
||||||
TO_REAL_STYLE: 'to-real-style',
|
TO_REAL_STYLE: 'to-real-style',
|
||||||
|
TO_REAL_VARIANTS: 'to-real-variants',
|
||||||
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',
|
||||||
@@ -39,9 +43,13 @@ export const NODE_DATATIER = {
|
|||||||
RESULT_IMAGE: 0,
|
RESULT_IMAGE: 0,
|
||||||
CARDS_SELECT: 0,
|
CARDS_SELECT: 0,
|
||||||
TO_REAL_STYLE: 1,
|
TO_REAL_STYLE: 1,
|
||||||
|
TO_REAL_VARIANTS: 1,
|
||||||
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,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
|
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modalWidth: {
|
modalWidth: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
})
|
})
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
let currentData = ref(null)
|
let currentData = ref(null)
|
||||||
const open = (data: any,) => {
|
const open = (data: any) => {
|
||||||
currentData.value = data
|
currentData.value = data
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
105
src/lang/en.ts
@@ -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: `Let’s 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: 'Let’s go, Fiphant!',
|
letsGo: 'Let’s 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',
|
||||||
@@ -214,7 +214,91 @@ export default {
|
|||||||
threeModelDesignAssistant:
|
threeModelDesignAssistant:
|
||||||
"🔄 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:
|
||||||
|
'🎨 I’ve 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...',
|
||||||
|
toRealVariantsShortcut1Value: 'Change the sofa backrest shape.',
|
||||||
|
toRealVariantsShortcut2Label: 'The chair legs...',
|
||||||
|
toRealVariantsShortcut2Value: 'The chair legs were modified.',
|
||||||
|
toRealVariantsShortcut3Label: 'Adjust the...',
|
||||||
|
toRealVariantsShortcut3Value: "Adjust the chair's armrest design.",
|
||||||
|
toRealVariantsShortcut4Label: 'Replace...',
|
||||||
|
toRealVariantsShortcut4Value: 'Replace tabletop shape with drawn shape. ',
|
||||||
|
toRealVariantsShortcut5Label: 'Modified the outer...',
|
||||||
|
toRealVariantsShortcut5Value: 'Modified the outer contour of the chair back.',
|
||||||
|
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'
|
||||||
@@ -222,7 +306,16 @@ export default {
|
|||||||
//3d面板
|
//3d面板
|
||||||
threeModel: {
|
threeModel: {
|
||||||
loading: 'Loading',
|
loading: 'Loading',
|
||||||
download: 'Download'
|
download: 'Download',
|
||||||
|
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',
|
||||||
|
|||||||
@@ -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: '取消',
|
||||||
@@ -209,7 +209,91 @@ export default {
|
|||||||
threeModelDesignAssistant:
|
threeModelDesignAssistant:
|
||||||
'🔄 我把你的效果图变成可以转着看的立体模型,你可以从各个角度检查一下结构。我建议重点看看转角、腿脚比例和座面厚度——这几个地方在草图里不容易发现问题,但打样的时候最容易出偏差,现在发现比较好改。',
|
'🔄 我把你的效果图变成可以转着看的立体模型,你可以从各个角度检查一下结构。我建议重点看看转角、腿脚比例和座面厚度——这几个地方在草图里不容易发现问题,但打样的时候最容易出偏差,现在发现比较好改。',
|
||||||
to3DViewDesignAssistant:
|
to3DViewDesignAssistant:
|
||||||
'📐 我们到最后一步了!我来帮你把 3D 模型导出为前视图、侧视图和俯视图!'
|
'📐 我们到最后一步了!我来帮你把 3D 模型导出为前视图、侧视图和俯视图!',
|
||||||
|
toRealVariantsDesignAssistant:
|
||||||
|
'🎨 我已经帮你把 3D 模型转换成多个视角的产品卡片,你可以选一张最顺手的,在上面直接涂鸦、改结构或加细节,再使用该功能一键生成全新的产品渲染图!',
|
||||||
|
toRealVariantsShortcut1Label: '修改...',
|
||||||
|
toRealVariantsShortcut1Value: '修改了沙发靠背的形状.',
|
||||||
|
toRealVariantsShortcut2Label: '椅子腿...',
|
||||||
|
toRealVariantsShortcut2Value: '修改了椅子腿的形状.',
|
||||||
|
toRealVariantsShortcut3Label: '更换...',
|
||||||
|
toRealVariantsShortcut3Value: "更换了椅子的扶手设计.",
|
||||||
|
toRealVariantsShortcut4Label: '替换...',
|
||||||
|
toRealVariantsShortcut4Value: '桌面形状完全换成画成的形状.',
|
||||||
|
toRealVariantsShortcut5Label: '修改...',
|
||||||
|
toRealVariantsShortcut5Value: '修改了椅背的外轮廓.',
|
||||||
|
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: '请输入'
|
||||||
@@ -217,7 +301,16 @@ export default {
|
|||||||
//3d面板
|
//3d面板
|
||||||
threeModel: {
|
threeModel: {
|
||||||
loading: '加载中',
|
loading: '加载中',
|
||||||
download: '下载'
|
download: '下载',
|
||||||
|
captureView: '捕获视图',
|
||||||
|
propertiesInformation: '属性信息',
|
||||||
|
_3DAsset: '3D资产',
|
||||||
|
fileFormat: '文件格式:glb',
|
||||||
|
transform: '变换',
|
||||||
|
dimensions: '尺寸',
|
||||||
|
width: '宽度',
|
||||||
|
height: '高度',
|
||||||
|
depth: '深度',
|
||||||
},
|
},
|
||||||
DepthCanvas: {
|
DepthCanvas: {
|
||||||
layer: '图层',
|
layer: '图层',
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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 || []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,13 +284,20 @@
|
|||||||
try {
|
try {
|
||||||
const urlParams = new URLSearchParams<AgentParamsType>({
|
const urlParams = new URLSearchParams<AgentParamsType>({
|
||||||
...params,
|
...params,
|
||||||
configParams: JSON.stringify(params.configParams)
|
// configParams: JSON.stringify(params.configParams)
|
||||||
|
configParams: JSON.stringify({
|
||||||
|
type: params.configParams.type || '',
|
||||||
|
region: params.configParams.region || '',
|
||||||
|
style: params.configParams.style || '',
|
||||||
|
temperature: params.configParams.temperature
|
||||||
|
})
|
||||||
})
|
})
|
||||||
const BASEURL = import.meta.env.VITE_APP_URL
|
const BASEURL = import.meta.env.VITE_APP_URL
|
||||||
|
|
||||||
const response = await fetch(`${BASEURL}${chatUrl}?${urlParams.toString()}`, {
|
const response = await fetch(`${BASEURL}${chatUrl}?${urlParams.toString()}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
signal: abortController.signal
|
signal: abortController.signal,
|
||||||
|
keepalive: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检查响应内容类型,判断是否为流式响应
|
// 检查响应内容类型,判断是否为流式响应
|
||||||
@@ -582,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
|
||||||
@@ -621,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) {
|
||||||
@@ -634,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
|
||||||
@@ -661,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
|
||||||
@@ -668,20 +736,21 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 防止插入重复图片
|
||||||
const mergeUniqueKeys = (targetArr, newData) => {
|
const mergeUniqueKeys = (targetArr, newData) => {
|
||||||
// 提取现有数组中所有的 key,存入 Set 以实现 O(1) 查询
|
|
||||||
const existingKeys = new Set(targetArr.flatMap((item) => Object.keys(item)))
|
const existingKeys = new Set(targetArr.flatMap((item) => Object.keys(item)))
|
||||||
|
|
||||||
Object.entries(newData).forEach(([key, value]) => {
|
Object.entries(newData).forEach(([key, value]) => {
|
||||||
if (!existingKeys.has(key)) {
|
if (!existingKeys.has(key)) {
|
||||||
targetArr.push({ [key]: value })
|
targetArr.push({ [key]: value })
|
||||||
existingKeys.add(key) // 防止 newData 内部有重复 key 时重复插入
|
existingKeys.add(key)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
setChatInfo
|
setChatInfo,
|
||||||
|
isGenerating
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
ref="containerRef"
|
||||||
class="preview-container flex"
|
class="preview-container flex"
|
||||||
:class="type === 'sketch' ? 'sketch-preview' : 'report-preview'"
|
:class="type === 'sketch' ? 'sketch-preview' : 'report-preview'"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
>
|
>
|
||||||
@@ -93,7 +94,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, onUnmounted, watch, computed } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted, watch, computed, nextTick } from 'vue'
|
||||||
import { deleteSketchFlowCanvas } from '@/api/flow-canvas'
|
import { deleteSketchFlowCanvas } from '@/api/flow-canvas'
|
||||||
import { useProjectStore } from '@/stores'
|
import { useProjectStore } from '@/stores'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
@@ -114,6 +115,7 @@
|
|||||||
|
|
||||||
// 存储每个图片的加载状态
|
// 存储每个图片的加载状态
|
||||||
const loadedStatus = ref<boolean[]>([])
|
const loadedStatus = ref<boolean[]>([])
|
||||||
|
const containerRef = ref<HTMLElement>()
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -131,7 +133,12 @@
|
|||||||
() => {
|
() => {
|
||||||
// 当 sketchList 变化时,重置加载状态
|
// 当 sketchList 变化时,重置加载状态
|
||||||
loadedStatus.value = new Array(combineSketchList.value.length).fill(false)
|
loadedStatus.value = new Array(combineSketchList.value.length).fill(false)
|
||||||
// pendingSketchIndexes.value = []
|
// 当 type 为 sketch 时,自动滚动到最底部
|
||||||
|
if (props.type === 'sketch') {
|
||||||
|
nextTick(() => {
|
||||||
|
containerRef.value?.scrollTo({ top: containerRef.value.scrollHeight, behavior: 'smooth' })
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -322,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 {
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
&-header {
|
&-header {
|
||||||
font-family: 'Medium';
|
font-family: 'Medium';
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
margin-bottom: 1.3rem;
|
// margin-bottom: 1.3rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -49,12 +51,13 @@
|
|||||||
const projectStore = useProjectStore()
|
const projectStore = useProjectStore()
|
||||||
|
|
||||||
const previewRef = ref(null)
|
const previewRef = ref(null)
|
||||||
|
const proJectId = computed(() => route.params.id)
|
||||||
const agentTitle = ref('Conversation')
|
const agentTitle = ref('Conversation')
|
||||||
const previewType = ref<'sketch' | 'report'>('sketch')
|
const previewType = ref<'sketch' | 'report'>('sketch')
|
||||||
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(
|
||||||
@@ -95,7 +103,7 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGetProjectInfoAndHistory = () => {
|
const handleGetProjectInfoAndHistory = () => {
|
||||||
sketchList.value = []
|
sketchList.value = []
|
||||||
getProjectInfo({ id: route.params.id }).then((res) => {
|
getProjectInfo({ id: route.params.id }).then((res) => {
|
||||||
if (!res) {
|
if (!res) {
|
||||||
@@ -116,8 +124,6 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const proJectId = computed(() => route.params.id)
|
|
||||||
|
|
||||||
const handleOpenReport = (data) => {
|
const handleOpenReport = (data) => {
|
||||||
previewRef.value.setSessionId(data.sessionId)
|
previewRef.value.setSessionId(data.sessionId)
|
||||||
previewRef.value.setReport(data.reportName, data.report)
|
previewRef.value.setReport(data.reportName, data.report)
|
||||||
@@ -145,10 +151,18 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleRenameConversation = (item) => {
|
||||||
|
if (String(item.id) === String(proJectId.value)) {
|
||||||
|
handleSetTitle(item.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
MyEvent.add('openReport', handleOpenReport)
|
MyEvent.add('openReport', handleOpenReport)
|
||||||
MyEvent.add('openUrls', handleOpenUrls)
|
MyEvent.add('openUrls', handleOpenUrls)
|
||||||
MyEvent.add('openSketch', handleOpenSketch)
|
MyEvent.add('openSketch', handleOpenSketch)
|
||||||
|
MyEvent.add('renameConversation', handleRenameConversation)
|
||||||
|
MyEvent.add('closeFlowCanvas', handleGetProjectInfoAndHistory)
|
||||||
projectStore.clearProject()
|
projectStore.clearProject()
|
||||||
if (proJectId.value) {
|
if (proJectId.value) {
|
||||||
handleGetProjectInfoAndHistory()
|
handleGetProjectInfoAndHistory()
|
||||||
@@ -157,7 +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('closeFlowCanvas', handleGetProjectInfoAndHistory)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -195,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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` - 设置选项
|
|
||||||
99
src/views/home/components/input/AgentOperatePopover.vue
Normal 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>
|
||||||
234
src/views/home/components/input/InputEditor.vue
Normal 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>
|
||||||
89
src/views/home/components/input/InputImagePreviewList.vue
Normal 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>
|
||||||
282
src/views/home/components/input/InputToolbar.vue
Normal 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>
|
||||||
46
src/views/home/components/input/ReportShortcutButton.vue
Normal 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>
|
||||||
160
src/views/home/components/input/SettingPopover.vue
Normal 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>
|
||||||
259
src/views/home/components/input/StyleSelect.vue
Normal 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>
|
||||||
48
src/views/home/components/input/options.ts
Normal 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 }
|
||||||
|
]
|
||||||
24
src/views/home/components/input/types.ts
Normal 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
|
||||||
|
}
|
||||||
552
src/views/home/components/input/useInputEditor.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/views/home/components/input/useInputImages.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -141,6 +141,7 @@
|
|||||||
const name = e.target.value
|
const name = e.target.value
|
||||||
if (!name) return console.warn('未输入名称,不允许重命名')
|
if (!name) return console.warn('未输入名称,不允许重命名')
|
||||||
item.name = name
|
item.name = name
|
||||||
|
MyEvent.emit('renameConversation', item)
|
||||||
updateProject(item.id, { name }).then(() => {
|
updateProject(item.id, { name }).then(() => {
|
||||||
GetProjectList()
|
GetProjectList()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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' })
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||