feat: 画布内聊天组件

This commit is contained in:
2026-03-05 17:12:35 +08:00
parent b13f7587c8
commit 88681aab88
7 changed files with 284 additions and 4 deletions

View File

@@ -0,0 +1,3 @@
<svg width="18" height="10" viewBox="0 0 18 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 1C18 0.447715 17.5523 0 17 0H1C0.447716 0 0 0.447715 0 1C0 1.55228 0.447716 2 1 2H17C17.5523 2 18 1.55228 18 1ZM18 9C18 8.44772 17.5523 8 17 8H7C6.44771 8 6 8.44772 6 9C6 9.55229 6.44771 10 7 10H17C17.5523 10 18 9.55229 18 9Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1,165 @@
<template>
<div class="assistant-container flex flex-col space-between" ref="containerRef">
<div class="assistant-header flex space-between align-center" @mousedown="handleMouseDown">
<div class="assistant-header-title">AI Assistant</div>
<SvgIcon name="canvas-assistant-menu" class="menu-icon" color="#D58C4D" />
</div>
<div class="assistant-body flex-1">
<List :messageList="messageList" />
</div>
<div class="assistant-input flex align-center">
<el-input v-model="inputMessage" :placeholder="$t('assistant.inputPlaceholder')" />
<div class="sender-btn flex flex-center" @click="handleSendAgent">
<img src="@/assets/images/sender.png" class="sender-icon" />
<!-- <div class="sender-pause" /> -->
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import List from './component/List.vue'
import { useDraggable } from './component/useDraggable'
const inputMessage = ref('')
const containerRef = ref<HTMLElement>()
// 使用拖拽 hook
const { handleMouseDown } = useDraggable(containerRef)
const messageList = ref([
{
content: 'Hi! How can I help you, today?',
id: 1,
role: 'assistant'
},
{
content: 'Please recommend some .',
id: 2,
role: 'user'
},
{
content: 'I noticed that you inputed a new sketch.',
id: 3,
role: 'system',
type: 'info'
},
{
content: 'Hi! How can I help you, today?',
id: 4,
role: 'assistant'
},
{
content: 'Please recommend some .',
id: 5,
role: 'user'
},
{
content: 'I noticed that you inputed a new sketch.',
id: 36,
role: 'system',
type: 'info'
},
{
content: 'Hi! How can I help you, today?',
id: 31,
role: 'assistant'
},
{
content: 'Please recommend some .',
id: 22,
role: 'user'
},
{
content: 'I noticed that you inputed a new sketch.',
id: 63,
role: 'system',
type: 'info'
}
])
</script>
<style lang="less" scoped>
.assistant-container {
position: absolute;
right: 6.2rem;
bottom: 13.2rem;
width: 46.69rem;
height: 56.6rem;
flex-shrink: 0;
// background-color: #fff;
box-shadow: 0px 19.44px 27.22px 0px #0000000d;
border: 1px solid;
border-image-source: linear-gradient(
119.03deg,
rgba(233, 121, 60, 0.3) 1.61%,
rgba(255, 207, 144, 0.3) 101.01%
);
border-radius: 1.69rem;
.assistant-header {
height: 5.66rem;
font-family: 'Regular';
font-size: 1.6rem;
padding: 0 1.59rem 0 2.05rem;
background: linear-gradient(
119.03deg,
rgba(233, 121, 60, 0.3) 1.61%,
rgba(255, 207, 144, 0.3) 101.01%
);
border-top-left-radius: 1.69rem;
border-top-right-radius: 1.69rem;
user-select: none;
cursor: grabbing;
&:active {
cursor: grabbing;
}
.menu-icon {
width: 1.8rem;
}
}
.assistant-body {
margin: 1.6rem 0 2.9rem;
padding: 0 1.95rem 0 2.5rem;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0;
display: none;
}
&::-webkit-scrollbar-thumb {
display: none;
}
&::-webkit-scrollbar-track {
display: none;
}
}
.assistant-input {
border: 1px solid #0000001a;
margin: 0 2rem 3rem;
border-radius: 3.53rem;
padding: 0 1rem 0 1.7rem;
.el-input {
:deep(.el-input__wrapper) {
background-color: transparent;
box-shadow: none;
padding: 0;
}
}
.sender-btn {
width: 2.2rem;
height: 2.2rem;
cursor: pointer;
background-color: #000;
border-radius: 50%;
.sender-icon {
width: 0.9rem;
height: 0.9rem;
}
.sender-pause {
width: 1rem;
height: 1rem;
background-color: #fff;
}
}
}
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<div class="list-wrapper flex flex-col">
<div
class="list-item flex align-center"
v-for="item in props.messageList"
:key="item.id"
:class="item.role"
>
<div class="list-item-content-text">{{ item.content }}</div>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
messageList: Array<any>
}>()
</script>
<style lang="less" scoped>
.list-wrapper {
row-gap: 1.6rem;
.list-item {
min-height: 3.86rem;
width: fit-content;
&.user {
width: 26.2rem;
align-self: end;
}
font-size: 1.4rem;
font-family: 'Regular';
color: #333;
padding: 1.1rem 1.7rem;
background: linear-gradient(#fffcf4, #fffcf4) padding-box,
linear-gradient(
119.03deg,
rgba(233, 121, 60, 0.3) 1.61%,
rgba(255, 207, 144, 0.3) 101.01%
)
border-box;
border: 1px solid transparent;
border-radius: 3.18rem;
}
}
</style>

View File

@@ -0,0 +1,61 @@
import { ref, onUnmounted, type Ref } from 'vue'
interface UseDraggableOptions {
onDragStart?: () => void
onDragEnd?: () => void
}
export function useDraggable(targetRef: Ref<HTMLElement | undefined>, options?: UseDraggableOptions) {
const isDragging = ref(false)
const startX = ref(0)
const startY = ref(0)
const startLeft = ref(0)
const startTop = ref(0)
const handleMouseDown = (e: MouseEvent) => {
// 只允许左键拖拽
if (e.button !== 0) return
isDragging.value = true
startX.value = e.clientX
startY.value = e.clientY
if (targetRef.value) {
const rect = targetRef.value.getBoundingClientRect()
startLeft.value = rect.left
startTop.value = rect.top
}
options?.onDragStart?.()
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging.value || !targetRef.value) return
const deltaX = e.clientX - startX.value
const deltaY = e.clientY - startY.value
targetRef.value.style.left = `${startLeft.value + deltaX}px`
targetRef.value.style.top = `${startTop.value + deltaY}px`
}
const handleMouseUp = () => {
isDragging.value = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
options?.onDragEnd?.()
}
onUnmounted(() => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
})
return {
isDragging,
handleMouseDown
}
}

View File

@@ -25,12 +25,14 @@
</div>
</div> -->
<flow-canvas />
<Assistant />
</div>
</template>
<script setup lang="ts">
import flowCanvas from './FlowCanvas/flow-canvas.vue'
import card from './FlowCanvas/components/nodes/cards/index.vue'
import Assistant from '../Assistant/assistant.vue'
import { computed, ref, markRaw, onMounted, reactive, nextTick } from 'vue'
const data = reactive({
x: 100,

View File

@@ -182,5 +182,8 @@ export default {
deleteCardConfirm: 'Are you sure you want to delete this function card?',
confirm: 'Confirm',
cancel: 'Cancel',
},
assistant: {
inputPlaceholder: 'Ask anything',
}
}

View File

@@ -80,7 +80,7 @@ export default {
timesPerHour: '{time} 次/小时',
userAgreement: '用户协议',
privacyPolicy: '隐私政策',
view: '查看',
view: '查看'
},
Country: {
unitedStates: '美国',
@@ -92,14 +92,14 @@ export default {
france: '法国',
japan: '日本',
canada: '加拿大',
germany: '德国',
germany: '德国'
},
Role: {
designer: '设计师',
student: '学生',
teacher: '教师',
parent: '家长',
other: '其他',
other: '其他'
},
Input: {
placeholder: '描述您想要设计的物品……例如:现代木质椅子,包豪斯风格的家具。',
@@ -176,6 +176,9 @@ export default {
flowCanvas: {
deleteCardConfirm: '确定要删除该功能卡片吗?',
confirm: '确认',
cancel: '取消',
cancel: '取消'
},
assistant: {
inputPlaceholder: '请输入',
}
}