|
@@ -0,0 +1,551 @@
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ class="editable-furniture"
|
|
|
+ :style="{
|
|
|
+ position: 'relative',
|
|
|
+ left: `${centerCss.left}px`,
|
|
|
+ top: `${centerCss.top}px`,
|
|
|
+ zIndex: 10,
|
|
|
+ pointerEvents: 'auto',
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="furniture-wrapper"
|
|
|
+ :style="wrapperStyle"
|
|
|
+ @mousedown.stop="startDrag"
|
|
|
+ @dblclick.stop="showPanel = !showPanel"
|
|
|
+ >
|
|
|
+ <furnitureIcon
|
|
|
+ :icon="localItem.type as FurnitureIconType"
|
|
|
+ :width="localItem.width"
|
|
|
+ :height="localItem.length"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="showPanel"
|
|
|
+ class="property-panel"
|
|
|
+ :style="{ left: `${panelPosition.left}px`, top: `${panelPosition.top}px` }"
|
|
|
+ @mousedown.stop
|
|
|
+ >
|
|
|
+ <div class="panel-header" @mousedown.prevent="startPanelDrag">
|
|
|
+ <div class="title">家具属性</div>
|
|
|
+ <div class="close" @click="showPanel = false">×</div>
|
|
|
+ <div class="capsule" @mousedown.prevent="startPanelDrag"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel-item">
|
|
|
+ <label>家具名称:</label>
|
|
|
+ <a-input v-model:value="localItem.name" size="small" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel-item">
|
|
|
+ <label>家具尺寸:</label>
|
|
|
+ <a-space>
|
|
|
+ <a-input-number
|
|
|
+ v-model:value="localItem.width"
|
|
|
+ :min="10"
|
|
|
+ size="small"
|
|
|
+ @change="onSizeChange"
|
|
|
+ />
|
|
|
+ <a-input-number
|
|
|
+ v-model:value="localItem.length"
|
|
|
+ :min="10"
|
|
|
+ size="small"
|
|
|
+ @change="onSizeChange"
|
|
|
+ />
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel-item">
|
|
|
+ <label>旋转角度:</label>
|
|
|
+ <a-select v-model:value="localItem.rotate" size="small">
|
|
|
+ <a-select-option :value="0">0°</a-select-option>
|
|
|
+ <a-select-option :value="90">90°</a-select-option>
|
|
|
+ <a-select-option :value="180">180°</a-select-option>
|
|
|
+ <a-select-option :value="270">270°</a-select-option>
|
|
|
+ </a-select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel-item">
|
|
|
+ <label>位置微调:</label>
|
|
|
+ <a-space>
|
|
|
+ <ArrowLeftOutlined @click="nudge('left')" />
|
|
|
+ <ArrowUpOutlined @click="nudge('up')" />
|
|
|
+ <ArrowDownOutlined @click="nudge('down')" />
|
|
|
+ <ArrowRightOutlined @click="nudge('right')" />
|
|
|
+ <a-input-number v-model:value="nudgeStep" :min="1" size="small" style="width: 60px" />
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel-item">
|
|
|
+ <label>平面坐标:</label>
|
|
|
+ <a-space>
|
|
|
+ x:<a-input-number v-model:value="localItem.x" @change="updateByFlatCoords" size="small" />
|
|
|
+ y:<a-input-number v-model:value="localItem.y" @change="updateByFlatCoords" size="small" />
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel-item">
|
|
|
+ <label>操作:</label>
|
|
|
+ <a-space>
|
|
|
+ <a-popconfirm title="确定要删除该家具吗?" @confirm="emit('delete', localItem.nanoid!)">
|
|
|
+ <a-button danger size="small">删除</a-button>
|
|
|
+ </a-popconfirm>
|
|
|
+ </a-space>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <pre class="debug">{{ localItem }}</pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { reactive, watch, ref, computed, onMounted, nextTick, type CSSProperties } from 'vue'
|
|
|
+import type { FurnitureItem } from '@/types/radar'
|
|
|
+import furnitureIcon from '../furnitureIcon/index.vue'
|
|
|
+import type { FurnitureIconType } from '@/types/furniture'
|
|
|
+import {
|
|
|
+ ArrowLeftOutlined,
|
|
|
+ ArrowUpOutlined,
|
|
|
+ ArrowDownOutlined,
|
|
|
+ ArrowRightOutlined,
|
|
|
+} from '@ant-design/icons-vue'
|
|
|
+import { useRadarCoordinateTransform } from '@/hooks/useRadarCoordinateTransform'
|
|
|
+
|
|
|
+defineOptions({ name: 'EditableFurniture' })
|
|
|
+
|
|
|
+interface Props {
|
|
|
+ item: FurnitureItem
|
|
|
+ angle: number
|
|
|
+ coordinates: [number, number, number, number]
|
|
|
+ initialAtOrigin?: boolean
|
|
|
+}
|
|
|
+const props = defineProps<Props>()
|
|
|
+const emit = defineEmits<{
|
|
|
+ (e: 'update', item: FurnitureItem & { left?: number; top?: number }): void
|
|
|
+ (e: 'delete', nanoid: string): void
|
|
|
+}>()
|
|
|
+
|
|
|
+const showPanel = ref(false)
|
|
|
+const nudgeStep = ref(5)
|
|
|
+
|
|
|
+// 扩展类型:在组件内部我们也维护页面像素 left/top(可选)
|
|
|
+type UIItem = FurnitureItem & { left?: number; top?: number }
|
|
|
+
|
|
|
+// localItem 保留业务语义 x/y 为业务左上角,同时维护 left/top 为页面像素(px)
|
|
|
+const localItem = reactive<UIItem>({
|
|
|
+ ...props.item,
|
|
|
+ x: props.item.x ?? 0,
|
|
|
+ y: props.item.y ?? 0,
|
|
|
+ width: props.item.width ?? 100,
|
|
|
+ length: props.item.length ?? 100,
|
|
|
+ rotate: props.item.rotate ?? 0,
|
|
|
+ left: (props.item as UIItem).left ?? 0,
|
|
|
+ top: (props.item as UIItem).top ?? 0,
|
|
|
+})
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.item,
|
|
|
+ (next) => {
|
|
|
+ if (!next) return
|
|
|
+
|
|
|
+ // 显式解构预期要合并的字段
|
|
|
+ const { x, y, width, length, rotate, name, type, nanoid, left, top } = next as Partial<UIItem>
|
|
|
+
|
|
|
+ if (x !== undefined) localItem.x = x
|
|
|
+ if (y !== undefined) localItem.y = y
|
|
|
+ if (width !== undefined) localItem.width = width
|
|
|
+ if (length !== undefined) localItem.length = length
|
|
|
+ if (rotate !== undefined) localItem.rotate = rotate
|
|
|
+ if (name !== undefined) localItem.name = name
|
|
|
+ if (type !== undefined) localItem.type = type
|
|
|
+ if (nanoid !== undefined) localItem.nanoid = nanoid
|
|
|
+ if (left !== undefined) localItem.left = left
|
|
|
+ if (top !== undefined) localItem.top = top
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+)
|
|
|
+
|
|
|
+// transforms(editor <-> radar <-> css)
|
|
|
+let transforms = useRadarCoordinateTransform(props.angle, props.coordinates)
|
|
|
+watch(
|
|
|
+ () => [props.angle, props.coordinates] as const,
|
|
|
+ () => {
|
|
|
+ transforms = useRadarCoordinateTransform(props.angle, props.coordinates)
|
|
|
+ nextTick()
|
|
|
+ .then(() => new Promise<void>((r) => requestAnimationFrame(() => r())))
|
|
|
+ .then(() => new Promise((r) => setTimeout(r, 0)))
|
|
|
+ .then(() => {
|
|
|
+ updateCssFromXY()
|
|
|
+ setPanelInitialPosition()
|
|
|
+ })
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+)
|
|
|
+
|
|
|
+// 渲染用中心点(DOM center,units: px)
|
|
|
+const centerCss = reactive({ left: 0, top: 0 })
|
|
|
+
|
|
|
+// 面板位置(相对于 outer 根容器,即相对于 centerCss)
|
|
|
+const panelPosition = reactive({ left: 0, top: 0 })
|
|
|
+
|
|
|
+const DECIMALS = 4
|
|
|
+const EPSILON = 1e-10
|
|
|
+function norm(n: number): number {
|
|
|
+ const r = Math.round(n * Math.pow(10, DECIMALS)) / Math.pow(10, DECIMALS)
|
|
|
+ return Math.abs(r) < EPSILON ? 0 : r
|
|
|
+}
|
|
|
+function normXY(x: number, y: number): { x: number; y: number } {
|
|
|
+ return { x: norm(x), y: norm(y) }
|
|
|
+}
|
|
|
+
|
|
|
+// 旋转矩阵(编辑器坐标:Y 向上为正)
|
|
|
+function rot(x: number, y: number, deg: number) {
|
|
|
+ const rad = (deg * Math.PI) / 180
|
|
|
+ return {
|
|
|
+ x: x * Math.cos(rad) - y * Math.sin(rad),
|
|
|
+ y: x * Math.sin(rad) + y * Math.cos(rad),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 计算四角相对中心偏移(editor 坐标)
|
|
|
+function rotatedCornersOffsets(rotate: number, w: number, h: number) {
|
|
|
+ return {
|
|
|
+ topLeft: rot(-w / 2, +h / 2, rotate),
|
|
|
+ topRight: rot(+w / 2, +h / 2, rotate),
|
|
|
+ bottomRight: rot(+w / 2, -h / 2, rotate),
|
|
|
+ bottomLeft: rot(-w / 2, -h / 2, rotate),
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ------------- 业务坐标系 <-> 编辑器坐标系 映射 -------------
|
|
|
+function businessToEditor(x: number, y: number) {
|
|
|
+ return rot(x, y, 90)
|
|
|
+}
|
|
|
+function editorToBusiness(x: number, y: number) {
|
|
|
+ return rot(x, y, -90)
|
|
|
+}
|
|
|
+// -----------------------------------------------------------
|
|
|
+
|
|
|
+// helper: 把业务左上角 -> 页面左上角(cssTopLeft)
|
|
|
+function businessTopLeftToCssTopLeft(bizX: number, bizY: number) {
|
|
|
+ const editorPt = businessToEditor(bizX, bizY)
|
|
|
+ const radarPt = transforms.editorToRadar(editorPt.x, editorPt.y)
|
|
|
+ const cssTopLeft = transforms.radarToCss(radarPt.x, radarPt.y)
|
|
|
+ return cssTopLeft
|
|
|
+}
|
|
|
+
|
|
|
+// updateCssFromXY:计算 cssTopLeft, 更新 centerCss,并写回 localItem.left/top(方案 A)
|
|
|
+function updateCssFromXY(): void {
|
|
|
+ const w = localItem.width ?? 0
|
|
|
+ const h = localItem.length ?? 0
|
|
|
+
|
|
|
+ const bizLeft = localItem.x ?? 0
|
|
|
+ const bizTop = localItem.y ?? 0
|
|
|
+
|
|
|
+ const cssTopLeft = businessTopLeftToCssTopLeft(bizLeft, bizTop)
|
|
|
+
|
|
|
+ // 写回页面像素(left/top),与 RadarView 的字段语义一致
|
|
|
+ localItem.left = cssTopLeft.left
|
|
|
+ localItem.top = cssTopLeft.top
|
|
|
+
|
|
|
+ // centerCss = cssTopLeft + half size (wrapper uses translate(-50%,-50%))
|
|
|
+ centerCss.left = cssTopLeft.left + w / 2
|
|
|
+ centerCss.top = cssTopLeft.top + h / 2
|
|
|
+}
|
|
|
+
|
|
|
+// 拖拽回写:centerCss -> cssTopLeft -> biz -> 回写 localItem.x/y;同时更新 left/top(页面像素)
|
|
|
+function updateXYFromCss(): void {
|
|
|
+ const w = localItem.width ?? 0
|
|
|
+ const h = localItem.length ?? 0
|
|
|
+
|
|
|
+ const cssTopLeftX = centerCss.left - w / 2
|
|
|
+ const cssTopLeftY = centerCss.top - h / 2
|
|
|
+
|
|
|
+ const radarPt = transforms.cssToRadar(cssTopLeftX, cssTopLeftY)
|
|
|
+ const editorPt = transforms.radarToEditor(radarPt.x, radarPt.y)
|
|
|
+ const bizPt = editorToBusiness(editorPt.x, editorPt.y)
|
|
|
+
|
|
|
+ // 回写业务左上角
|
|
|
+ localItem.x = bizPt.x
|
|
|
+ localItem.y = bizPt.y
|
|
|
+ ;({ x: localItem.x, y: localItem.y } = normXY(localItem.x!, localItem.y!))
|
|
|
+
|
|
|
+ // 同步页面像素字段(保持和 RadarView 一致)
|
|
|
+ localItem.left = cssTopLeftX
|
|
|
+ localItem.top = cssTopLeftY
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化面板位置(相对于 outer,即相对于 centerCss)
|
|
|
+function setPanelInitialPosition() {
|
|
|
+ const w = localItem.width ?? 0
|
|
|
+ const h = localItem.length ?? 0
|
|
|
+ panelPosition.left = Math.round(w / 2 + 50)
|
|
|
+ panelPosition.top = Math.round(-h / 2)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ // 使用父组件传入的业务左上角(若有)
|
|
|
+ localItem.x = props.item.x ?? localItem.x
|
|
|
+ localItem.y = props.item.y ?? localItem.y
|
|
|
+ ;({ x: localItem.x, y: localItem.y } = normXY(localItem.x!, localItem.y!))
|
|
|
+
|
|
|
+ if (props.initialAtOrigin) {
|
|
|
+ localItem.x = 0
|
|
|
+ localItem.y = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ nextTick()
|
|
|
+ .then(() => new Promise<void>((r) => requestAnimationFrame(() => r())))
|
|
|
+ .then(() => updateCssFromXY())
|
|
|
+ .then(() => {
|
|
|
+ setPanelInitialPosition()
|
|
|
+ // emit initial state with left/top populated
|
|
|
+ emit('update', { ...localItem })
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+// wrapper style: 以 centerCss 为外层定位点,内部以中心旋转
|
|
|
+const wrapperStyle = computed(
|
|
|
+ () =>
|
|
|
+ ({
|
|
|
+ width: `${localItem.width}px`,
|
|
|
+ height: `${localItem.length}px`,
|
|
|
+ transform: `translate(-50%, -50%) rotate(${localItem.rotate}deg)`,
|
|
|
+ transformOrigin: '50% 50%',
|
|
|
+ cursor: 'move',
|
|
|
+ position: 'absolute',
|
|
|
+ left: '0px',
|
|
|
+ top: '0px',
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ }) as CSSProperties
|
|
|
+)
|
|
|
+
|
|
|
+function updateByFlatCoords(): void {
|
|
|
+ ;({ x: localItem.x, y: localItem.y } = normXY(localItem.x!, localItem.y!))
|
|
|
+ updateCssFromXY()
|
|
|
+ emit('update', { ...localItem })
|
|
|
+}
|
|
|
+
|
|
|
+function onSizeChange() {
|
|
|
+ updateCssFromXY()
|
|
|
+ setPanelInitialPosition()
|
|
|
+ emit('update', { ...localItem })
|
|
|
+}
|
|
|
+
|
|
|
+// 旋转监听:保持业务中心不动并更新左上角与渲染中心,写回 left/top
|
|
|
+watch(
|
|
|
+ () => localItem.rotate,
|
|
|
+ (angle, oldAngle) => {
|
|
|
+ const w = localItem.width ?? 0
|
|
|
+ const h = localItem.length ?? 0
|
|
|
+
|
|
|
+ const offsetsOld = rotatedCornersOffsets(oldAngle ?? angle, w, h)
|
|
|
+ const topLeftOldEditor = offsetsOld.topLeft
|
|
|
+ const topLeftOldBiz = editorToBusiness(topLeftOldEditor.x, topLeftOldEditor.y)
|
|
|
+ const centerBizX = localItem.x! - topLeftOldBiz.x
|
|
|
+ const centerBizY = localItem.y! - topLeftOldBiz.y
|
|
|
+
|
|
|
+ const offsetsNew = rotatedCornersOffsets(angle, w, h)
|
|
|
+ const topLeftNewEditor = offsetsNew.topLeft
|
|
|
+ const topLeftNewBiz = editorToBusiness(topLeftNewEditor.x, topLeftNewEditor.y)
|
|
|
+
|
|
|
+ localItem.x = centerBizX + topLeftNewBiz.x
|
|
|
+ localItem.y = centerBizY + topLeftNewBiz.y
|
|
|
+ ;({ x: localItem.x, y: localItem.y } = normXY(localItem.x!, localItem.y!))
|
|
|
+
|
|
|
+ const editorCenter = businessToEditor(centerBizX, centerBizY)
|
|
|
+ const radarCenter = transforms.editorToRadar(editorCenter.x, editorCenter.y)
|
|
|
+ const css = transforms.radarToCss(radarCenter.x, radarCenter.y)
|
|
|
+
|
|
|
+ // 更新 centerCss + left/top
|
|
|
+ centerCss.left = css.left
|
|
|
+ centerCss.top = css.top
|
|
|
+ localItem.left = css.left - w / 2
|
|
|
+ localItem.top = css.top - h / 2
|
|
|
+
|
|
|
+ emit('update', { ...localItem })
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+// 拖拽家具:直接修改 centerCss 并回写业务坐标与 left/top
|
|
|
+function startDrag(e: MouseEvent): void {
|
|
|
+ e.preventDefault()
|
|
|
+ e.stopPropagation()
|
|
|
+ const startX = e.clientX
|
|
|
+ const startY = e.clientY
|
|
|
+ const initLeft = centerCss.left
|
|
|
+ const initTop = centerCss.top
|
|
|
+
|
|
|
+ const onMove = (ev: MouseEvent) => {
|
|
|
+ centerCss.left = initLeft + (ev.clientX - startX)
|
|
|
+ centerCss.top = initTop + (ev.clientY - startY)
|
|
|
+ updateXYFromCss()
|
|
|
+ // emit with updated business coords and page left/top
|
|
|
+ emit('update', { ...localItem })
|
|
|
+ }
|
|
|
+ const onUp = () => {
|
|
|
+ window.removeEventListener('mousemove', onMove)
|
|
|
+ window.removeEventListener('mouseup', onUp)
|
|
|
+ document.body.style.userSelect = ''
|
|
|
+ }
|
|
|
+ window.addEventListener('mousemove', onMove)
|
|
|
+ window.addEventListener('mouseup', onUp)
|
|
|
+ document.body.style.userSelect = 'none'
|
|
|
+}
|
|
|
+
|
|
|
+// 面板拖拽:只移动 panelPosition(相对于 outer,不影响家具)
|
|
|
+function startPanelDrag(e: MouseEvent) {
|
|
|
+ e.preventDefault()
|
|
|
+ e.stopPropagation()
|
|
|
+ const startX = e.clientX
|
|
|
+ const startY = e.clientY
|
|
|
+ const initLeft = panelPosition.left
|
|
|
+ const initTop = panelPosition.top
|
|
|
+
|
|
|
+ const onMove = (ev: MouseEvent) => {
|
|
|
+ panelPosition.left = initLeft + (ev.clientX - startX)
|
|
|
+ panelPosition.top = initTop + (ev.clientY - startY)
|
|
|
+ }
|
|
|
+ const onUp = () => {
|
|
|
+ window.removeEventListener('mousemove', onMove)
|
|
|
+ window.removeEventListener('mouseup', onUp)
|
|
|
+ }
|
|
|
+ window.addEventListener('mousemove', onMove)
|
|
|
+ window.addEventListener('mouseup', onUp)
|
|
|
+}
|
|
|
+
|
|
|
+// 微调
|
|
|
+function nudge(direction: 'left' | 'right' | 'up' | 'down'): void {
|
|
|
+ switch (direction) {
|
|
|
+ case 'left':
|
|
|
+ localItem.x! -= nudgeStep.value
|
|
|
+ break
|
|
|
+ case 'right':
|
|
|
+ localItem.x! += nudgeStep.value
|
|
|
+ break
|
|
|
+ case 'up':
|
|
|
+ localItem.y! += nudgeStep.value
|
|
|
+ break
|
|
|
+ case 'down':
|
|
|
+ localItem.y! -= nudgeStep.value
|
|
|
+ break
|
|
|
+ }
|
|
|
+ ;({ x: localItem.x, y: localItem.y } = normXY(localItem.x!, localItem.y!))
|
|
|
+ updateCssFromXY()
|
|
|
+ emit('update', { ...localItem })
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+.editable-furniture {
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ position: relative;
|
|
|
+ .furniture-wrapper {
|
|
|
+ cursor: move;
|
|
|
+ }
|
|
|
+
|
|
|
+ .property-panel {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ padding: 5px 12px 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ z-index: 1000;
|
|
|
+ width: 260px;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
|
+ user-select: none;
|
|
|
+
|
|
|
+ .panel-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ position: relative;
|
|
|
+ user-select: none;
|
|
|
+
|
|
|
+ .capsule {
|
|
|
+ cursor: move;
|
|
|
+ position: absolute;
|
|
|
+ top: -5px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ border-radius: 10px;
|
|
|
+ width: 100px;
|
|
|
+ height: 8px;
|
|
|
+ background: rgba(230, 230, 230, 0.8);
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ cursor: move;
|
|
|
+ }
|
|
|
+
|
|
|
+ .title {
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .close {
|
|
|
+ font-size: 22px;
|
|
|
+ color: #999;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: color 0.2s;
|
|
|
+ &:hover {
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 6px;
|
|
|
+
|
|
|
+ label {
|
|
|
+ flex: 0 0 80px;
|
|
|
+ color: #666;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ant-input-number),
|
|
|
+ :deep(.ant-select),
|
|
|
+ :deep(.ant-input) {
|
|
|
+ flex: 1;
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.ant-space) {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ gap: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.anticon) {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #333;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: color 0.2s;
|
|
|
+ &:hover {
|
|
|
+ color: #1890ff;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .panel-footer {
|
|
|
+ margin-top: 10px;
|
|
|
+ text-align: center;
|
|
|
+
|
|
|
+ :deep(.ant-btn) {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|