<template>
<div
v-show="selectState.height && selectState.width"
:class="['canvas-rect select', { 'multi-select': multiStateLength > 1 }, { dragging: isMultiDragging }]"
:style="{
top: selectState.top + 'px',
left: selectState.left + 'px',
height: selectState.height + 'px',
width: selectState.width + 'px'
}"
>
<div v-if="showQuickAction" ref="labelRef" class="corner-mark-left" :style="labelStyle">
<span>{{ selectState.componentName }}</span>
<TinyPopover
v-model="showPopover"
placement="top-start"
title="快捷设置"
width="310"
popper-class="short-cut-set"
trigger="manual"
>
<shortCutPopover v-if="showPopover" @active="activeSetting('props')"></shortCutPopover>
<template #reference>
<icon-setting class="icon-setting svg-currentcolor" @click.stop="showPopover = !showPopover"></icon-setting>
</template>
</TinyPopover>
</div>
<template v-else>
<div
:class="[showAction && 'drag-resize', 'resize-top']"
draggable="true"
@mousedown.stop="onMousedown($event, 'center', 'start')"
></div>
<div
:class="[showAction && 'drag-resize', 'resize-bottom']"
draggable="true"
@mousedown.stop="onMousedown($event, 'center', 'end')"
></div>
<div
:class="[showAction && 'drag-resize', 'resize-left']"
draggable="true"
@mousedown.stop="onMousedown($event, 'start', 'center')"
></div>
<div
:class="[showAction && 'drag-resize', 'resize-right']"
draggable="true"
@mousedown.stop="onMousedown($event, 'end', 'center')"
></div>
<div
:class="[showAction && 'drag-resize', 'resize-left']"
draggable="true"
@mousedown.stop="onMousedown($event, 'start', 'start')"
></div>
<div
:class="[showAction && 'drag-resize', 'resize-top-right']"
draggable="true"
@mousedown.stop="onMousedown($event, 'end', 'start')"
></div>
<div
:class="[showAction && 'drag-resize', 'resize-bottom-left']"
draggable="true"
@mousedown.stop="onMousedown($event, 'start', 'end')"
></div>
<div
:class="[showAction && 'drag-resize', 'resize-bottom-right']"
draggable="true"
@mousedown.stop="onMousedown($event, 'end', 'end')"
></div>
</template>
<div v-if="showAction" ref="optionRef" class="corner-mark-right" :style="fixStyle">
<template v-if="!isModal">
<div v-if="showToParent" title="选择父级">
<icon-chevron-left class="svg-currentcolor" @click.stop="selectParent"></icon-chevron-left>
</div>
<div title="向上移动">
<icon-arrow-up class="svg-currentcolor" @click.stop="moveUp"></icon-arrow-up>
</div>
<div title="向下移动">
<icon-arrow-down class="svg-currentcolor" @click.stop="moveDown"></icon-arrow-down>
</div>
<div title="复制">
<icon-copy class="svg-currentcolor" @click.stop="copy"></icon-copy>
</div>
</template>
<template v-else>
<div title="隐藏">
<icon-eyeclose class="svg-currentcolor" @click.stop="hide"></icon-eyeclose>
</div>
</template>
<div title="删除">
<icon-del class="svg-currentcolor" @click.stop="remove"></icon-del>
</div>
<div title="AI助手" class="ai-helper">
<TinyPopover
v-model="showAIPopover"
placement="bottom-start"
width="360"
popper-class="ai-popper"
trigger="manual"
:visible-arrow="false"
>
<CanvasAIChat
v-if="shouldShowAIChat"
class="ai-component"
@complete="handleAIChatComplete"
@close="closeAIHelper"
></CanvasAIChat>
<AILoadingDialog
v-if="shouldShowAILoading"
class="ai-component"
@cancel="handleAILoadingCancel"
></AILoadingDialog>
<AIConfirmDialog
v-if="shouldShowAIConfirm"
class="ai-component"
@confirm="handleAIConfirm"
@cancel="handleAICancel"
@refresh="handleAIRefresh"
></AIConfirmDialog>
<template #reference>
<svg-icon name="add-group" @click.stop="openAIHelper"></svg-icon>
</template>
</TinyPopover>
</div>
</div>
</div>
<div v-show="hoverState.height && hoverState.width" class="canvas-rect hover">
<div class="corner-mark-left">
{{ hoverState.componentName }}
</div>
<div v-show="hoverState.configure?.isContainer" class="corner-mark-bottom-right">拖放元素到容器内</div>
</div>
<div v-show="inactiveHoverState.height && inactiveHoverState.width" class="canvas-rect inactive-hover">
<div class="corner-mark-left">
{{ inactiveHoverState.componentName }}
</div>
</div>
<div v-show="lineState.height && lineState.width" class="canvas-rect line">
<div :class="['hover-line', lineState.position, { forbidden: lineState.forbidden }]">
<div v-if="lineState.position === 'in' && hoverState.configure" class="choose-slots"></div>
</div>
</div>
</template>
<script>
import { watchPostEffect, ref, watch, computed, nextTick } from 'vue'
import {
IconDel,
IconSetting,
IconChevronLeft,
IconArrowDown,
IconArrowUp,
IconCopy,
IconEyeclose
} from '@opentiny/vue-icon'
import {
canvasState,
getCurrent,
removeNodeById,
selectNode,
updateRect,
copyNode,
getRenderer,
dragStart,
getCurrentElement,
querySelectById
} from '../container'
import { useLayout, useMaterial, useCanvas, useMessage } from '@opentiny/tiny-engine-meta-register'
import { Popover } from '@opentiny/vue'
import shortCutPopover from './shortCutPopover.vue'
import CanvasAIChat from './CanvasAIChat.vue'
import AIConfirmDialog from './AIConfirmDialog.vue'
import AILoadingDialog from './AILoadingDialog.vue'
import { chat } from '../services/agentServices'
import { utils } from '@opentiny/tiny-engine-utils'
import useAIChat from '../composables/useAIChat'
const { deepClone } = utils
const OPTION_BAR_HEIGHT = 24
const LABEL_HEIGHT = 24
const SCROLL_BAR_WIDTH = 8
const OPTION_SPACE = 6
const SELECTION_BORDER_WIDTH = 2
const STYLE_UNSET = 'unset'
export default {
components: {
AILoadingDialog,
CanvasAIChat,
AIConfirmDialog,
IconDel: IconDel(),
IconSetting: IconSetting(),
IconChevronLeft: IconChevronLeft(),
IconArrowDown: IconArrowDown(),
IconArrowUp: IconArrowUp(),
IconCopy: IconCopy(),
IconEyeclose: IconEyeclose(),
shortCutPopover,
TinyPopover: Popover
},
props: {
hoverState: {
type: Object,
default: () => ({})
},
inactiveHoverState: {
type: Object,
default: () => ({})
},
selectState: {
type: Object,
default: () => ({})
},
lineState: {
type: Object,
default: () => ({})
},
multiStateLength: {
type: Number,
default: () => 0
},
resize: {
type: Boolean,
default: false
},
isMultiDragging: {
type: Boolean,
default: false
},
windowGetClickEventTarget: Object
},
emits: ['remove', 'selectSlot', 'setting'],
setup(props) {
const { pageState: _pageState, getNode } = useCanvas()
const {
getNodeAIStatus,
openNodeAIChat,
closeNodeAIHelper,
startNodeAILoading,
cancelNodeAILoading,
shouldShowNodeAIChat,
shouldShowNodeAILoading,
shouldShowNodeAIConfirm,
confirmNodeAIAction,
updateNodeAIStatus,
cancelNodeAIAction,
applyAIPatches,
buildAIChatRequest
} = useAIChat()
let aiChatAbortController = null
let aiChatRequestToken = 0
const remove = () => {
removeNodeById(getCurrent().schema?.id)
}
const moveChild = (list, selected, addend) => {
if (!list || list.length < 2) {
return
}
const index = list.indexOf(selected)
if (index > -1) {
const toIndex = index + addend
if (toIndex > -1 && toIndex < list.length) {
;[list[index], list[toIndex]] = [list[toIndex], list[index]]
useMessage().publish({ topic: 'schemaChange', data: {} })
updateRect()
}
}
}
const moveUp = () => {
const { parent, schema } = getCurrent()
moveChild(parent?.children, schema, -1)
}
const moveDown = () => {
const { parent, schema } = getCurrent()
moveChild(parent?.children, schema, 1)
}
const selectParent = () => {
const parent = getCurrent().parent
const parentId = parent?.id
if (parent?.componentName === 'Template' && !querySelectById(parentId)) {
const grandParent = useCanvas().getNodeWithParentById(parentId)?.parent
if (grandParent) {
selectNode(grandParent?.id)
}
} else if (parentId) {
selectNode(parentId)
}
}
const copy = () => {
copyNode(getCurrent().schema.id)
}
const hide = () => {
if (getCurrent().schema?.id) {
const { clearSelect } = useCanvas().canvasApi.value
getRenderer().setCondition(getCurrent().schema.id, false)
useCanvas().pageState.nodesStatus[getCurrent().schema.id] = false
clearSelect()
}
updateRect()
}
const isSingleNode = computed(() => {
return props.multiStateLength < 2
})
const showAction = computed(() => {
const { schema, parent } = getCurrent()
if (schema?.props?.['data-id'] === 'root-container') {
return false
}
return !props.resize && parent && parent?.type !== 'JSSlot' && isSingleNode.value
})
const showQuickAction = computed(() => {
return !props.resize && isSingleNode.value
})
const showToParent = computed(() => getCurrent().parent !== useCanvas().getSchema())
const isModal = computed(() => {
const config = useMaterial().getMaterial(props.selectState.componentName)
return config?.configure?.isModal
})
const shouldShowAIChat = computed(() => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return false
}
return shouldShowNodeAIChat(currentSchema.id)
})
const shouldShowAIConfirm = computed(() => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return false
}
return shouldShowNodeAIConfirm(currentSchema.id)
})
const shouldShowAILoading = computed(() => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return false
}
return shouldShowNodeAILoading(currentSchema.id)
})
const showAIPopover = ref(false)
watch(
() => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return false
}
const status = getNodeAIStatus(currentSchema?.id)
return status?.state !== 'hidden' && !status?.collapsed
},
(val) => {
showAIPopover.value = val
}
)
const openAIHelper = () => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return
}
const currentStatus = getNodeAIStatus(currentSchema.id)
if (currentStatus && currentStatus.collapsed) {
updateNodeAIStatus(currentSchema.id, { collapsed: false })
} else if (currentStatus && currentStatus.state !== 'hidden') {
updateNodeAIStatus(currentSchema.id, { collapsed: true })
} else {
openNodeAIChat(currentSchema.id)
}
}
const closeAIHelper = () => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return
}
const currentStatus = getNodeAIStatus(currentSchema.id)
if (currentStatus && (currentStatus.state === 'loading' || currentStatus.state === 'confirm')) {
updateNodeAIStatus(currentSchema.id, { collapsed: true })
} else {
closeNodeAIHelper(currentSchema.id)
}
}
const optionRef = ref(null)
const fixStyle = ref('')
const showPopover = ref(false)
const activeSetting = () => {
showPopover.value = false
}
const findParentHasClass = (target) => {
const parent = target.parentNode
if (parent.className === undefined) {
return false
}
const name = JSON.stringify(parent.className)
const preventClassNameList = ['short-cut-set', 'tiny-dialog-box', 'icon-popover', 'i18n-input-popover']
if (preventClassNameList.some((item) => name?.includes(item))) {
return true
}
return findParentHasClass(parent)
}
const onMousedown = (event, horizontal, vertical) => {
const element = getCurrentElement()
if (!element) {
return
}
const { x, y, height, width } = element.getBoundingClientRect()
dragStart(getCurrent().schema, element, {
offsetX: event.clientX,
offsetY: event.clientY,
x,
y,
height,
width,
horizontal,
vertical
})
}
watch(
() => props.windowGetClickEventTarget,
(newProps) => {
if (newProps) {
const flag = findParentHasClass(newProps)
if (!flag) {
showPopover.value = false
}
}
}
)
const labelRef = ref(null)
const labelStyle = ref('')
const positions = {
LEFT: 'left',
RIGHT: 'right',
TOP: 'top',
BOTTOM: 'bottom',
isHorizontal(position) {
return [this.LEFT, this.RIGHT].includes(position)
},
isVertical(position) {
return [this.TOP, this.BOTTOM].includes(position)
}
}
class Align {
alignLeft = false
horizontalValue = 0
alignTop = false
verticalValue = 0
constructor({ alignLeft, horizontalValue, alignTop, verticalValue }) {
this.alignLeft = alignLeft
this.horizontalValue = horizontalValue
this.alignTop = alignTop
this.verticalValue = verticalValue
}
align(position, value = 0) {
if (positions.isHorizontal(position)) {
this.alignLeft = position === positions.LEFT
this.horizontalValue = value
return this
}
if (positions.isVertical(position)) {
this.alignTop = position === positions.TOP
this.horizontalValue = value
return this
}
return this
}
toStyleValue() {
const styleObj = {}
if (this.alignLeft) {
styleObj.left = this.horizontalValue
styleObj.right = STYLE_UNSET
} else {
styleObj.right = this.horizontalValue
styleObj.left = STYLE_UNSET
}
if (this.alignTop) {
styleObj.top = this.verticalValue
styleObj.bottom = STYLE_UNSET
} else {
styleObj.bottom = this.verticalValue
styleObj.top = STYLE_UNSET
}
return this.styleObj2Str(styleObj)
}
styleObj2Str = (styleObj) => {
return Object.entries(styleObj)
.map(([key, value]) => {
const num = Number(value)
if (Number.isNaN(num)) {
return `${key}:${value}`
}
const val = positions.isHorizontal(key) ? num - SELECTION_BORDER_WIDTH : num
return `${key}:${val}px`
})
.join(';')
}
}
* 检查元素在画布中的可用空间
* @param {number} top - 选中元素顶部位置
* @param {number} selectedHeight - 选中元素高度
* @param {number} canvasHeight - 画布高度
* @param {number} elementHeight - 要放置元素的高度
* @returns {{hasTopSpace: boolean, hasBottomSpace: boolean}}
*/
const checkElementSpace = (top, selectedHeight, canvasHeight, elementHeight) => {
return {
hasTopSpace: top >= elementHeight,
hasBottomSpace: canvasHeight - top - selectedHeight >= elementHeight
}
}
* 根据策略决定元素应该放置在顶部还是底部
* @param {number} top - 选中元素顶部位置
* @param {number} elementHeight - 要放置元素的高度
* @param {boolean} hasTopSpace - 顶部是否有足够空间
* @param {boolean} hasBottomSpace - 底部是否有足够空间
* @param {string} strategy - 放置策略 ('topFirst' | 'bottomFirst')
* @returns {boolean} 是否放置在底部
*/
const determineElementPosition = (hasTopSpace, hasBottomSpace, strategy = 'topFirst') => {
if (strategy === 'bottomFirst') {
return hasBottomSpace || !hasTopSpace
} else {
return !hasTopSpace && hasBottomSpace
}
}
* 通用的垂直对齐计算函数
* @param {boolean} isAtBottom - 是否放置在底部
* @param {number} elementHeight - 元素高度
* @param {boolean} hasTopSpace - 顶部是否有足够空间
* @param {boolean} hasBottomSpace - 底部是否有足够空间
* @param {boolean} bottomFirst - 是否底部优先策略(true: Option策略, false: Label策略)
* @returns {{alignTop: boolean, verticalValue: number}}
*/
const calculateVerticalAlignment = (
isAtBottom,
elementHeight,
hasTopSpace,
hasBottomSpace,
bottomFirst = false
) => {
const alignTop = !isAtBottom
let verticalValue
if (bottomFirst) {
verticalValue = !isAtBottom || hasBottomSpace ? -elementHeight : 0
} else {
verticalValue = isAtBottom || hasTopSpace ? -elementHeight : 0
}
return { alignTop, verticalValue }
}
* 一站式元素对齐计算函数(组合了空间检查、位置决策、对齐计算)
* @param {number} top - 选中元素顶部位置
* @param {number} selectedHeight - 选中元素高度
* @param {number} canvasHeight - 画布高度
* @param {number} elementHeight - 要放置元素的高度
* @param {string} strategy - 放置策略 ('topFirst' | 'bottomFirst')
* @returns {{alignTop: boolean, verticalValue: number}}
*/
const calculateElementAlignment = (top, selectedHeight, canvasHeight, elementHeight, strategy = 'topFirst') => {
const spaceInfo = checkElementSpace(top, selectedHeight, canvasHeight, elementHeight)
const isAtBottom = determineElementPosition(spaceInfo.hasTopSpace, spaceInfo.hasBottomSpace, strategy)
return calculateVerticalAlignment(
isAtBottom,
elementHeight,
spaceInfo.hasTopSpace,
spaceInfo.hasBottomSpace,
strategy === 'bottomFirst'
)
}
const getStyleValues = (selectState, canvasSize, labelWidth, optionWidth) => {
const { left, top, width, height, doc } = selectState
const { width: canvasWidth, height: canvasHeight } = canvasSize
const fullRectWidth = labelWidth + optionWidth + OPTION_SPACE
const labelAlignment = calculateElementAlignment(
top,
height,
canvasHeight,
LABEL_HEIGHT,
'topFirst'
)
const labelAlign = new Align({
alignLeft: true,
horizontalValue: 0,
alignTop: labelAlignment.alignTop,
verticalValue: labelAlignment.verticalValue
})
if (!doc) {
return {}
}
const optionAlignment = calculateElementAlignment(
top,
height,
canvasHeight,
OPTION_BAR_HEIGHT,
'bottomFirst'
)
const optionAlign = new Align({
alignLeft: false,
horizontalValue: 0,
alignTop: optionAlignment.alignTop,
verticalValue: optionAlignment.verticalValue
})
const scrollBarWidth = doc.documentElement.scrollHeight > doc.documentElement.clientHeight ? SCROLL_BAR_WIDTH : 0
if (width < fullRectWidth) {
const isLabelAlignRight = labelWidth > width && left + labelWidth + scrollBarWidth > canvasWidth
if (isLabelAlignRight) {
labelAlign.align(positions.RIGHT)
}
const isOptionAlignLeft = optionWidth > width && left + width - optionWidth < 0
if (isOptionAlignLeft) {
optionAlign.align(positions.LEFT)
}
if (labelAlignment.alignTop === optionAlignment.alignTop) {
if (left + fullRectWidth < canvasWidth) {
labelAlign.align(positions.LEFT)
optionAlign.align(positions.LEFT, labelWidth + OPTION_SPACE)
} else {
optionAlign.align(positions.RIGHT)
labelAlign.align(positions.RIGHT, optionWidth + OPTION_SPACE)
}
}
} else {
if (left < 0) {
labelAlign.align(positions.LEFT, Math.min(-left, width - fullRectWidth))
}
if (left + width + scrollBarWidth > canvasWidth) {
optionAlign.align(
positions.RIGHT,
Math.min(left + width + scrollBarWidth - canvasWidth, width - fullRectWidth)
)
}
}
return {
labelStyleValue: labelAlign.toStyleValue(),
optionStyleValue: optionAlign.toStyleValue()
}
}
watchPostEffect(async () => {
const { left, top, width, height, doc } = props.selectState
if (!showQuickAction.value) {
return
}
await nextTick()
if (labelRef.value && !optionRef.value) {
labelStyle.value = `left: 0; right: unset; top: unset; bottom: 0`
return
}
if (!labelRef.value || !optionRef.value) {
return
}
const scale = useLayout().getScale()
const canvasRect = canvasState.iframe.getBoundingClientRect()
const { width: labelWidth } = labelRef.value.getBoundingClientRect()
const { width: optionWidth } = optionRef.value.getBoundingClientRect()
const { labelStyleValue, optionStyleValue } = getStyleValues(
{ left, top, width, height, doc },
{ width: canvasRect.width / scale, height: canvasRect.height / scale },
labelWidth / scale,
optionWidth / scale
)
labelStyle.value = labelStyleValue
fixStyle.value = optionStyleValue
})
const handleAIChatComplete = async (content) => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return
}
startNodeAILoading(currentSchema.id, 'AI正在处理您的请求...')
const currentNode = getNode(currentSchema.id)
if (currentNode) {
const currentStatus = getNodeAIStatus(currentSchema.id)
if (currentStatus) {
currentStatus.originalNodeData = deepClone(currentNode)
}
}
aiChatAbortController = new AbortController()
const currentToken = ++aiChatRequestToken
try {
const params = await buildAIChatRequest(content)
const response = await chat(params, aiChatAbortController.signal)
if (currentToken !== aiChatRequestToken) {
return
}
const status = getNodeAIStatus(currentSchema.id)
if (!status || status.state !== 'loading') {
return
}
if (!applyAIPatches(currentSchema.id, response, content)) {
cancelNodeAILoading(currentSchema.id)
}
} catch (error) {
if (error.name === 'AbortError' || error.name === 'CanceledError') {
return
}
cancelNodeAILoading(currentSchema.id)
}
}
const handleAILoadingCancel = () => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return
}
if (aiChatAbortController) {
aiChatAbortController.abort()
aiChatAbortController = null
aiChatRequestToken++
}
cancelNodeAILoading(currentSchema.id)
}
const handleAIRefresh = async () => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return
}
const nodeId = currentSchema.id
const currentAIStatus = getNodeAIStatus(nodeId)
if (!currentAIStatus) {
return
}
if (currentAIStatus.originalNodeData) {
const { restoreNodeSubtree } = useCanvas()
restoreNodeSubtree(nodeId, deepClone(currentAIStatus.originalNodeData))
useMessage().publish({ topic: 'schemaChange', data: { nodeId } })
}
updateNodeAIStatus(nodeId, {
aiModifiedNodeData: undefined
})
startNodeAILoading(nodeId, 'AI正在重新生成...')
const chatContent = currentAIStatus.chatContent
if (!chatContent) {
cancelNodeAILoading(nodeId)
return
}
try {
const params = await buildAIChatRequest(chatContent)
const refreshToken = ++aiChatRequestToken
aiChatAbortController = new AbortController()
const response = await chat(params, aiChatAbortController.signal)
if (refreshToken !== aiChatRequestToken) {
return
}
const status = getNodeAIStatus(nodeId)
if (!status || status.state !== 'loading') {
return
}
if (!applyAIPatches(nodeId, response, chatContent)) {
cancelNodeAILoading(nodeId)
}
} catch (error) {
if (error.name === 'AbortError' || error.name === 'CanceledError') {
return
}
cancelNodeAILoading(nodeId)
}
}
const handleAIConfirm = () => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return
}
confirmNodeAIAction(currentSchema.id)
}
const handleAICancel = () => {
const currentSchema = getCurrent().schema
if (!currentSchema?.id) {
return
}
cancelNodeAIAction(currentSchema.id)
}
return {
remove,
moveUp,
moveDown,
copy,
hide,
selectParent,
optionRef,
fixStyle,
showAction,
showQuickAction,
showPopover,
showToParent,
showAIPopover,
activeSetting,
isModal,
onMousedown,
labelStyle,
labelRef,
openAIHelper,
shouldShowAIChat,
shouldShowAILoading,
shouldShowAIConfirm,
closeAIHelper,
handleAIChatComplete,
handleAILoadingCancel,
handleAIConfirm,
handleAICancel,
handleAIRefresh
}
}
}
</script>
<style lang="less">
.canvas-rect {
position: absolute;
box-sizing: border-box;
pointer-events: none;
border: 1px solid var(--te-canvas-container-border-color-checked);
z-index: 2;
// 禁止文本选择
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
&.absolute {
pointer-events: all;
}
&.hover {
border-style: dashed;
top: v-bind("hoverState.top + 'px'");
left: v-bind("hoverState.left + 'px'");
height: v-bind("hoverState.height + 'px'");
width: v-bind("hoverState.width + 'px'");
.corner-mark-left {
height: 14px;
top: -14px;
padding-left: 0;
font-size: 12px;
}
}
&.inactive-hover {
border-style: dashed;
top: v-bind("inactiveHoverState.top + 'px'");
left: v-bind("inactiveHoverState.left + 'px'");
height: v-bind("inactiveHoverState.height + 'px'");
width: v-bind("inactiveHoverState.width + 'px'");
border-color: var(--te-canvas-container-border-color-hover);
.corner-mark-left {
height: 14px;
top: -14px;
padding-left: 0;
font-size: 12px;
color: var(--te-canvas-container-text-color-weaken);
}
}
&.line {
border-color: transparent;
top: v-bind("lineState.top + 'px'");
left: v-bind("lineState.left + 'px'");
height: v-bind("lineState.height + 'px'");
width: v-bind("lineState.width + 'px'");
}
.hover-line {
&.top {
width: 100%;
height: 5px;
background: var(--te-canvas-container-text-color-checked);
position: absolute;
top: -3px;
}
&.left {
width: 5px;
height: 100%;
background: var(--te-canvas-container-text-color-checked);
position: absolute;
left: -3px;
}
&.bottom {
width: 100%;
height: 5px;
background: var(--te-canvas-container-text-color-checked);
position: absolute;
bottom: -3px;
}
&.right {
width: 5px;
height: 100%;
background: var(--te-canvas-container-text-color-checked);
position: absolute;
right: -3px;
}
&.in {
width: 100%;
height: 100%;
background: var(--te-canvas-container-hover-line-in-bg-color);
}
&.forbidden:not(.in) {
background: var(--te-canvas-container-hover-line-forbid-bg-color);
}
&.forbidden.in {
background: var(--te-canvas-container-hover-line-in-forbid-bg-color);
}
}
.choose-slots {
display: flex;
justify-content: left;
align-items: left;
height: 100%;
& > div {
pointer-events: all;
width: 40px;
border: 1px solid var(--te-canvas-container-border-color-checked);
color: var(--te-canvas-container-choose-slot-text-color);
overflow: hidden;
font-size: 10px;
margin: 2px;
text-align: center;
&:hover {
background: #40a9ff;
color: #fff;
}
}
}
.corner-mark-left {
display: flex;
align-items: center;
font-size: 14px;
position: absolute;
top: -24px;
height: 24px;
color: var(--te-canvas-container-corner-mark-left-text-color);
padding: 0 8px;
.icon-setting {
margin-left: 4px;
margin-bottom: 2px;
}
}
.corner-mark-bottom-right {
position: absolute;
font-size: 12px;
right: -1px;
color: var(--te-canvas-container-text-color-white);
bottom: -20px;
background: var(--te-canvas-container-bg-color-checked);
padding: 0 2px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.corner-mark-right {
display: flex;
align-items: center;
position: absolute;
height: 24px;
padding: 0 4px;
color: var(--te-canvas-container-text-color-white);
background: var(--te-canvas-container-bg-color-checked);
pointer-events: all;
cursor: pointer;
div {
font-size: 0;
svg {
margin: 0 4px;
font-size: 16px;
}
}
}
&.select {
z-index: 3;
border-width: 2px;
.corner-mark-left {
white-space: nowrap;
pointer-events: all;
color: var(--te-canvas-container-text-color-white);
background: var(--te-canvas-container-bg-color-checked);
svg {
cursor: pointer;
}
}
}
&.multi-select {
border-color: var(--te-canvas-container-border-color-checked);
border-style: solid;
border-width: 2px;
.corner-mark-left {
background-color: var(--te-canvas-container-bg-color-checked);
color: var(--te-canvas-container-text-color-white);
}
&.dragging {
opacity: 0.7;
border-color: var(--te-canvas-container-border-color-multi, #1890ff);
border-style: dashed;
border-width: 2px;
background-color: rgba(24, 144, 255, 0.15);
box-shadow: 0 0 12px rgba(24, 144, 255, 0.4);
transition: all 0.2s ease;
animation: pulse-border 1.5s infinite;
.corner-mark-left {
background-color: var(--te-canvas-container-border-color-multi, #1890ff);
animation: pulse-bg 1.5s infinite;
}
}
}
}
@keyframes pulse-border {
0% {
box-shadow: 0 0 0 0 rgba(24, 144, 255, 0.4);
}
70% {
box-shadow: 0 0 0 6px rgba(24, 144, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(24, 144, 255, 0);
}
}
@keyframes pulse-bg {
0% {
background-color: rgba(24, 144, 255, 1);
}
50% {
background-color: rgba(24, 144, 255, 0.7);
}
100% {
background-color: rgba(24, 144, 255, 1);
}
}
.short-cut-set.short-cut-set.tiny-popper.tiny-popover {
.tiny-popover__title {
color: var(--te-canvas-container-text-color-primary);
font-size: 14px;
}
}
.drag-resize {
position: absolute;
top: -6px;
bottom: -6px;
left: -6px;
right: -6px;
height: 6px;
width: 6px;
background-color: #409eff;
cursor: pointer;
pointer-events: auto !important;
&.resize-top {
left: calc(50% - 3px);
cursor: n-resize;
}
&.resize-bottom {
left: calc(50% - 3px);
top: auto;
cursor: s-resize;
}
&.resize-left {
top: calc(50% - 3px);
cursor: e-resize;
}
&.resize-right {
top: calc(50% - 3px);
left: auto;
cursor: e-resize;
}
&.resize-top-left {
cursor: nw-resize;
}
&.resize-top-right {
left: auto;
cursor: ne-resize;
}
&.resize-bottom-left {
top: auto;
cursor: sw-resize;
}
&.resize-bottom-right {
left: auto;
top: auto;
cursor: se-resize;
}
}
.ai-helper {
position: relative;
.ai-component,
.ai-component-loading {
position: absolute;
right: 0;
width: 360px;
opacity: 0;
transform: translateY(-10px);
animation: slideIn 0.2s ease-in-out forwards;
}
.ai-component-loading {
width: 270px;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(10px);
}
}
.tiny-popover.tiny-popover.tiny-popper[x-placement].ai-popper {
padding: 0;
border-radius: 40px;
}
</style>