<script setup lang="ts">
/**
 * WhiteboardEditor - 白板编辑器
 *
 * 基于三层架构：
 * - BoardModel（数据模型）
 * - BoardRenderer（渲染层）
 * - BoardInteractionManager（交互系统）
 */

import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useMessage, NButton, NSpace, NDropdown, NModal, NInput, NInputNumber, NColorPicker, NSlider, NDivider, NPopover, NIcon, NTooltip, NSelect } from 'naive-ui'
import { Icon } from '@iconify/vue'
import { useWhiteboard } from './useWhiteboard'
import { getRecord, updateRecord, uploadFileToJingrow } from '@/shared/api/common'
import { slugToPageType } from '@/shared/utils/slug'
import type { ToolType, ElementType, BoardElement, ElementStyle, BoardPage, ArrowEnd, LineStyle, TextAlign, TextBaseline } from './core/types'
import { STICKY_NOTE_COLORS } from './core/types'
import { t } from '@/shared/i18n'
import { mermaidToElements } from './mermaidToElements'
import { useRealtime, type PgUpdateData } from '@/shared/composables/useRealtime'

// ---- 路由 & 消息 ----
const route = useRoute()
const router = useRouter()
const message = useMessage()

const recordName = ref<string>((route.query.id as string) || '')

// ---- Canvas 引用 ----
const canvasRef = ref<HTMLCanvasElement>()
const pageTabInputRef = ref<HTMLInputElement>()
const imageFileInputRef = ref<HTMLInputElement>()
const mermaidFileInputRef = ref<HTMLInputElement>()

// ---- 白板逻辑 ----
const {
  board,
  renderer,
  interaction,
  currentTool,
  initCanvas,
  addElement,
  deleteSelected,
  updateElement,
  updateElementStyle,
  updateElementPosition,
  updateElementSize,
  bringToFront,
  sendToBack,
  addConnection,
  removeConnection,
  pages,
  activePageId,
  addPage,
  setActivePage,
  removePage,
  renamePage,
  zoomIn,
  zoomOut,
  resetZoom,
  fitView,
  currentZoom,
  setTool,
  getSelectedIds,
  setSelectedIds,
  clearSelection,
  selectedIds,
  exportData,
  importData,
  canUndo,
  canRedo,
  undo,
  redo,
  saveHistory,
  resize,
  cancelDrawing,
  bindBoardChangeListener,
  stickyNoteColors,
} = useWhiteboard({
  onElementDblClick: (elementId) => {
    if (elementId) startTextEdit(elementId)
  },
  onEraserHover: (element) => {
    eraserHoverElement.value = element
  }
})

// ---- 状态 ----
const loading = ref(false)
const saving = ref(false)
const exporting = ref(false)
const showExportMenu = ref(false)
const pageTitle = ref('Whiteboard')

// ---- 远程同步（Socket.IO） ----
const isSaving = ref(false)
const realtime = useRealtime()

function handlePgUpdate(data: PgUpdateData): void {
  if (isSaving.value) return
  if (data.pagetype !== 'Whiteboard' || data.name !== recordName.value) return
  if (data.modified && data.modified === board.value.modified) return
  const localData = JSON.stringify(board.value.serialize())
  const vp = renderer.value?.getViewport()
  getRecord('Whiteboard', recordName.value).then((result) => {
    if (!result.success || !result.data) return
    const rec = result.data as any
    if (!rec.whiteboard_data) return
    const remoteRaw = rec.whiteboard_data
    const remoteData = typeof remoteRaw === 'string' ? JSON.parse(remoteRaw) : remoteRaw
    if (JSON.stringify(remoteData) !== localData) {
      importData(remoteData)
      board.value.modified = data.modified || ''
      // 恢复用户视口（importData 会使用数据中的视口，这里需覆盖回用户当前视口）
      if (vp) {
        renderer.value?.setViewport(vp)
        currentZoom.value = vp.zoom ?? 1
      }
      // 清除陈旧选中状态（旧 board 的元素 ID 在新 board 中可能不存在）
      clearSelection()
      message.info(t('Content updated by another user.'))
    }
  }).catch(() => {})
}

// ---- Mermaid 弹窗 ----
const showMermaidModal = ref(false)
const mermaidCode = ref('')
const mermaidPreview = ref('')
const mermaidPreviewError = ref('')
const mermaidInserting = ref(false)
const mermaidPreviewRef = ref<HTMLDivElement>()
let mermaidDebounceTimer: ReturnType<typeof setTimeout> | null = null

function openMermaidModal(): void {
  showExportMenu.value = false
  showMermaidModal.value = true
  mermaidCode.value = ''
  mermaidPreview.value = ''
  mermaidPreviewError.value = ''
}

async function renderMermaidPreview(): Promise<void> {
  if (mermaidDebounceTimer) clearTimeout(mermaidDebounceTimer)
  mermaidDebounceTimer = setTimeout(async () => {
    if (!mermaidCode.value.trim()) {
      mermaidPreview.value = ''
      mermaidPreviewError.value = ''
      return
    }
    const id = `mermaid-preview-${Date.now()}`
    try {
      const { default: mermaid } = await import('mermaid')
      const { svg } = await mermaid.render(id, mermaidCode.value)
      mermaidPreview.value = svg
      mermaidPreviewError.value = ''
    } catch (e: any) {
      mermaidPreview.value = ''
      mermaidPreviewError.value = e?.message || t('Render error')
    }
  }, 500)
}

async function insertMermaidFromModal(): Promise<void> {
  if (!mermaidCode.value.trim()) return
  mermaidInserting.value = true
  try {
    const vp = renderer.value?.getViewport() ?? { zoom: 1, panX: 0, panY: 0 }
    const canvasSize = renderer.value?.getCanvasSize() ?? { width: 800, height: 600 }
    const cx = canvasSize.width / 2 / vp.zoom - vp.panX / vp.zoom
    const cy = canvasSize.height / 2 / vp.zoom - vp.panY / vp.zoom

    const result = await mermaidToElements(mermaidCode.value, { offsetX: cx, offsetY: cy })

    if (result && result.nodes.length > 0) {
      const nodeIdMap = new Map<string, string>()
      const addedIds: string[] = []
      for (const node of result.nodes) {
        const el = addElement(node.type, {
          id: node.id,
          x: node.x,
          y: node.y,
          width: node.width,
          height: node.height,
          text: node.text,
          style: {
            fill: node.fill ?? undefined,
            stroke: node.stroke ?? undefined,
          },
        } as any)
        nodeIdMap.set(node.id, el.id)
        addedIds.push(el.id)
      }
      for (const edge of result.edges) {
        const startMapped = nodeIdMap.get(edge.startId)
        const endMapped = nodeIdMap.get(edge.endId)
        if (startMapped && endMapped) {
          const startEl = board.value.getElement(startMapped)
          const endEl = board.value.getElement(endMapped)
          if (startEl && endEl) {
            // Compute bounding box from waypoints so eraser hover highlight (which uses el.x/y/width/height) renders correctly.
            // The render path already uses line geometry for hit-testing regardless of bounding box.
            const allPoints = [edge.startPoint, ...edge.points, edge.endPoint]
            const xs = allPoints.map(p => p.x)
            const ys = allPoints.map(p => p.y)
            const arrowX = Math.min(...xs)
            const arrowY = Math.min(...ys)
            const arrowWidth = Math.max(...xs) - arrowX
            const arrowHeight = Math.max(...ys) - arrowY
            addElement('arrow', {
              x: arrowX,
              y: arrowY,
              width: Math.max(arrowWidth, 1),
              height: Math.max(arrowHeight, 1),
              startPoint: edge.startPoint,
              endPoint: edge.endPoint,
              waypoints: edge.points.slice(1, -1),
              connectionStyle: {
                stroke: '#64748b',
                strokeWidth: 2,
                lineStyle: edge.strokeStyle ?? 'solid',
                endArrow: 'arrow',
              },
              label: edge.label,
            } as any)
          }
        }
      }
      if (addedIds.length > 0) {
        setSelectedIds([addedIds[0]])
      }
    } else {
      message.error(t('Could not parse Mermaid diagram — check your syntax'))
    }
    saveHistory('add-mermaid')
    showMermaidModal.value = false
  } catch (e) {
    message.error(t('Failed to insert Mermaid diagram'))
  } finally {
    mermaidInserting.value = false
  }
}

function handleMermaidFileSelect(event: Event): void {
  const input = event.target as HTMLInputElement
  const file = input.files?.[0]
  if (!file) return
  const reader = new FileReader()
  reader.onload = (e) => {
    mermaidCode.value = (e.target?.result as string) || ''
    renderMermaidPreview()
  }
  reader.readAsText(file)
  input.value = ''
}

// ---- 文本编辑 ----
const editingTextId = ref<string | null>(null)
const editText = ref('')
const editTextPosition = ref({ x: 0, y: 0, width: 200, height: 40 })
// 编辑时 textarea 样式属性（与 canvas 渲染 BoardRenderer 对齐）
const editTextStyle = ref({ fontSize: 20, fontWeight: '400', color: '#1e293b', textAlign: 'center' as TextAlign, textBaseline: 'middle' as TextBaseline, transformY: 0 })

// ---- 全屏 ----
const isFullscreen = ref(false)
const editorRoot = ref<HTMLElement | null>(null)

// ---- 橡皮擦悬停高亮 ----
const eraserHoverElement = ref<{ x: number; y: number; width: number; height: number } | null>(null)

function toggleFullscreen(): void {
  const el = editorRoot.value
  if (!el) return
  if (!isFullscreen.value) {
    el.requestFullscreen?.() || el.webkitRequestFullscreen?.()
  } else {
    document.exitFullscreen?.() || document.webkitExitFullscreen?.()
  }
}

function handleFullscreenChange(): void {
  isFullscreen.value = !!(document.fullscreenElement || document.webkitFullscreenElement)
  nextTick(() => {
    resize()
  })
}

// ---- 属性面板 ----
// 当前选中的单个元素（方便模板中使用）
const selectedElement = computed(() => {
  if (selectedIds.value.length !== 1) return null
  return board.value.getElement(selectedIds.value[0]) ?? null
})

// 是否为纯文本元素
const isTextElement = computed(() => selectedElement.value?.type === 'text')

// 是否为含文本的形状（矩形/椭圆/菱形/便利贴）
const isTextShapeElement = computed(() => {
  if (!selectedElement.value) return false
  const t = selectedElement.value.type
  return t === 'rectangle' || t === 'ellipse' || t === 'diamond' || t === 'sticky-note'
})

// 是否显示文字控制（纯文本或含文本的形状）
const hasTextControl = computed(() => isTextElement.value || isTextShapeElement.value)

// 是否显示填充色（不含线条类元素）
const hasFillControl = computed(() => {
  if (!selectedElement.value) return false
  const t = selectedElement.value.type
  return t !== 'line' && t !== 'arrow' && t !== 'freehand'
})

// 是否显示描边控制（形状类元素）
const hasStrokeControl = computed(() => {
  if (!selectedElement.value) return false
  const t = selectedElement.value.type
  return t !== 'text' && t !== 'sticky-note'
})

// 是否显示边框宽度（形状类元素）
const hasStrokeWidthControl = computed(() => {
  if (!selectedElement.value) return false
  const t = selectedElement.value.type
  return t !== 'text' && t !== 'sticky-note' && t !== 'freehand'
})

// 是否显示圆角（矩形/图片）
const hasBorderRadiusControl = computed(() => {
  if (!selectedElement.value) return false
  const t = selectedElement.value.type
  return t === 'rectangle' || t === 'image'
})

// 元素类型标签映射
const elementTypeLabel = computed(() => {
  if (!selectedElement.value) return ''
  const type = selectedElement.value.type
  if (type === 'line') return t('Line Tool')
  if (type === 'sticky-note') return t('Sticky Note')
  return t(type.charAt(0).toUpperCase() + type.slice(1))
})

// 元素类型图标映射
const elementTypeIcon = computed(() => {
  if (!selectedElement.value) return 'tabler:box'
  const t = selectedElement.value.type
  if (t === 'rectangle') return 'tabler:rectangle'
  if (t === 'ellipse') return 'tabler:circle'
  if (t === 'diamond') return '◇'
  if (t === 'sticky-note') return 'tabler:note'
  if (t === 'text') return 'tabler:typography'
  if (t === 'arrow') return 'tabler:arrow-right'
  if (t === 'line') return 'tabler:line'
  if (t === 'freehand') return 'tabler:pencil'
  if (t === 'image') return 'tabler:photo'
  return 'tabler:box'
})

// 是否为箭头/线条元素
const isArrowElement = computed(() => {
  const t = selectedElement.value?.type
  return t === 'arrow' || t === 'line'
})

// 是否显示箭头控制（线条类元素）
const hasArrowControl = computed(() => isArrowElement.value)

// 获取当前连接的样式
const currentRouteStyle = computed(() => {
  const el = selectedElement.value as any
  if (!el || !isArrowElement.value) return 'straight'
  return el.connectionStyle?.route ?? 'straight'
})

const currentEndArrow = computed(() => {
  const el = selectedElement.value as any
  if (!el || !isArrowElement.value) return 'arrow-open'
  return el.connectionStyle?.endArrow ?? 'arrow-open'
})

// 当前描边色：arrow/line 从 connectionStyle 读，普通形状从 style 读
const currentStroke = computed(() => {
  const el = selectedElement.value as any
  if (!el) return '#334155'
  if (el.type === 'arrow' || el.type === 'line') {
    return el.connectionStyle?.stroke ?? '#3b82f6'
  }
  return el.style?.stroke ?? '#334155'
})

// 当前描边宽度：arrow/line 从 connectionStyle 读，普通形状从 style 读
const currentStrokeWidth = computed(() => {
  const el = selectedElement.value as any
  if (!el) return 2
  if (el.type === 'arrow' || el.type === 'line') {
    return el.connectionStyle?.strokeWidth ?? 2
  }
  return el.style?.strokeWidth ?? 2
})

// 当前线条风格：arrow/line 从 connectionStyle 读，普通形状从 style 读
const currentLineStyle = computed(() => {
  const el = selectedElement.value as any
  if (!el) return 'solid'
  if (el.type === 'arrow' || el.type === 'line') {
    return el.connectionStyle?.lineStyle ?? 'solid'
  }
  return el.style?.lineStyle ?? 'solid'
})

// 线条风格选项（可视化图标按钮组）
const lineStyleOptions: { label: string; value: LineStyle; icon: string }[] = [
  {
    label: t('Solid'),
    value: 'solid',
    icon: `<svg width="40" height="16" viewBox="0 0 40 16" xmlns="http://www.w3.org/2000/svg"><line x1="2" y1="8" x2="38" y2="8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>`,
  },
  {
    label: t('Dashed'),
    value: 'dashed',
    icon: `<svg width="40" height="16" viewBox="0 0 40 16" xmlns="http://www.w3.org/2000/svg"><line x1="2" y1="8" x2="38" y2="8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-dasharray="8 5"/></svg>`,
  },
  {
    label: t('Dotted'),
    value: 'dotted',
    icon: `<svg width="40" height="16" viewBox="0 0 40 16" xmlns="http://www.w3.org/2000/svg"><line x1="2" y1="8" x2="38" y2="8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-dasharray="2 4"/></svg>`,
  },
  {
    label: t('Dash-Dot'),
    value: 'dash-dot',
    icon: `<svg width="40" height="16" viewBox="0 0 40 16" xmlns="http://www.w3.org/2000/svg"><line x1="2" y1="8" x2="38" y2="8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-dasharray="8 4 2 4"/></svg>`,
  },
]

// 文字水平对齐选项
const textAlignOptions: { label: string; value: TextAlign; icon: string }[] = [
  {
    label: t('Align Left'),
    value: 'left',
    icon: `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 3H14M2 6H10M2 9H12M2 12H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
  },
  {
    label: t('Align Center'),
    value: 'center',
    icon: `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 3H14M4 6H12M3 9H13M5 12H11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
  },
  {
    label: t('Align Right'),
    value: 'right',
    icon: `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 3H14M6 6H14M4 9H14M8 12H14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
  },
]

// 文字垂直对齐选项
const textBaselineOptions: { label: string; value: TextBaseline; icon: string }[] = [
  {
    label: t('Align Top'),
    value: 'top',
    icon: `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.67 2.67L13.33 2.67" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M6 7.33C6 6.6 6.6 6 7.33 6H8.67C9.4 6 10 6.6 10 7.33V12.67C10 13.4 9.4 14 8.67 14H7.33C6.6 14 6 13.4 6 12.67V7.33Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
  },
  {
    label: t('Align Middle'),
    value: 'middle',
    icon: `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 2.67V5.33" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M8 10.67V13.33" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M4 7.33C4 6.6 4.6 6 5.33 6H10.67C11.4 6 12 6.6 12 7.33V8.67C12 9.4 11.4 10 10.67 10H5.33C4.6 10 4 9.4 4 8.67V7.33Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
  },
  {
    label: t('Align Bottom'),
    value: 'bottom',
    icon: `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.67 13.33L13.33 13.33" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M6 3.33C6 2.6 6.6 2 7.33 2H8.67C9.4 2 10 2.6 10 3.33V8.67C10 9.4 9.4 10 8.67 10H7.33C6.6 10 6 9.4 6 8.67V3.33Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
  },
]

// 当前文字水平对齐
const currentTextAlign = computed(() => {
  const el = selectedElement.value as any
  if (!el) return 'center'
  return el.style?.textAlign ?? 'center'
})

// 当前文字垂直对齐
const currentTextBaseline = computed(() => {
  const el = selectedElement.value as any
  if (!el) return 'middle'
  return el.style?.textBaseline ?? 'middle'
})

// 箭头端点选项（可视化图标按钮组）
const arrowEndOptions: { label: string; value: ArrowEnd; icon: string }[] = [
  {
    label: t('Arrow Open'),
    value: 'arrow-open',
    icon: `<svg width="28" height="16" viewBox="0 0 28 16" fill="none" xmlns="http://www.w3.org/2000/svg"><line x1="2" y1="8" x2="20" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M20 8L14 4M20 8L14 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`,
  },
  {
    label: t('Arrow Filled'),
    value: 'arrow',
    icon: `<svg width="28" height="16" viewBox="0 0 28 16" fill="none" xmlns="http://www.w3.org/2000/svg"><line x1="2" y1="8" x2="20" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M20 8L15 4.5V11.5L20 8Z" fill="currentColor"/></svg>`,
  },
]

// 预设描边宽度选项
const strokeWidthOptions = [
  { label: '1', value: 1 },
  { label: '2', value: 2 },
  { label: '3', value: 3 },
  { label: '4', value: 4 },
  { label: '6', value: 6 },
  { label: '8', value: 8 },
]

// 预设透明度选项
const opacityMarks = {
  0: '0%',
  25: '25%',
  50: '50%',
  75: '75%',
  100: '100%',
}

// 填充色预设调色板
const fillSwatches = [
  '#ffffff', '#f1f5f9', '#e2e8f0', '#cbd5e1',
  '#fef08a', '#bbf7d0', '#bfdbfe', '#fecaca',
  '#e9d5ff', '#f9a8d4', '#fdba74', '#6ee7b7',
]

// 描边色预设调色板
const strokeSwatches = [
  '#334155', '#64748b', '#94a3b8', '#1e293b',
  '#3b82f6', '#ef4444', '#22c55e', '#f59e0b',
  '#8b5cf6', '#ec4899', '#14b8a6', '#f97316',
]

// 预设圆角
const borderRadiusPresets = [
  { label: '0', value: 0 },
  { label: '4', value: 4 },
  { label: '8', value: 8 },
  { label: '12', value: 12 },
  { label: '16', value: 16 },
  { label: '9999', value: 9999 },
]

// ---- 工具栏 ----
const toolGroups = computed(() => [
  {
    label: t('Tools'),
    items: [
      { label: t('Select'), key: 'select', icon: 'mdi:select' },
      { label: t('Hand'), key: 'hand', icon: 'tabler:hand-move' },
    ]
  },
  {
    label: t('Shapes'),
    items: [
      { label: t('Rectangle'), key: 'rectangle', icon: 'tabler:rectangle' },
      { label: t('Ellipse'), key: 'ellipse', icon: 'tabler:circle' },
      { label: t('Diamond'), key: 'diamond', icon: '◇' },
      { label: t('Sticky Note'), key: 'sticky-note', icon: 'tabler:note' },
    ]
  },
  {
    label: t('Lines'),
    items: [
      { label: t('Arrow'), key: 'arrow', icon: 'tabler:arrow-right' },
      { label: t('Line Tool'), key: 'line', icon: 'tabler:line' },
    ]
  },
  {
    label: t('Draw'),
    items: [
      { label: t('Freehand'), key: 'freehand', icon: 'tabler:pencil' },
      { label: t('Text'), key: 'text', icon: 'tabler:letter-t' },
      { label: t('Eraser'), key: 'eraser', icon: 'tabler:eraser' },
      { label: t('Insert Image'), key: 'image-insert', icon: 'tabler:photo' },
    ]
  },
])

function handleToolClick(tool: string): void {
  if (tool === 'image-insert') {
    imageFileInputRef.value?.click()
    return
  }
  setTool(tool as ToolType)
}

// ---- 图片插入 ----
function handleImageFileSelect(event: Event): void {
  const input = event.target as HTMLInputElement
  const file = input.files?.[0]
  if (!file) return
  insertImage(file)
  input.value = ''
}

async function insertImage(file: File): Promise<void> {
  if (!file.type.startsWith('image/')) {
    message.warning(t('Please select an image file'))
    return
  }
  const maxSize = 10 * 1024 * 1024
  if (file.size > maxSize) {
    message.warning(t('Image size must be less than 10MB'))
    return
  }

  // 先创建占位元素，显示加载状态
  const vp = renderer.value?.getViewport() ?? { zoom: 1, panX: 0, panY: 0 }
  const canvasSize = renderer.value?.getCanvasSize() ?? { width: 800, height: 600 }
  const cx = canvasSize.width / 2 / vp.zoom - vp.panX / vp.zoom
  const cy = canvasSize.height / 2 / vp.zoom - vp.panY / vp.zoom
  const placeholderEl = addElement('image', {
    x: cx - 150,
    y: cy - 100,
    width: 300,
    height: 200,
    src: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
  } as any)
  setSelectedIds([placeholderEl.id])

  // 上传到 jingrow 文件系统
  const uploadResult = await uploadFileToJingrow(file, 'Whiteboard', recordName.value || undefined)
  if (!uploadResult.success || !uploadResult.file_url) {
    message.error(uploadResult.error || t('Failed to upload image'))
    board.value.removeElement(placeholderEl.id)
    return
  }

  // 获取图片实际尺寸
  const img = new Image()
  img.onload = () => {
    let w = img.naturalWidth
    let h = img.naturalHeight
    const maxDim = 600
    if (w > maxDim || h > maxDim) {
      const ratio = Math.min(maxDim / w, maxDim / h)
      w = Math.round(w * ratio)
      h = Math.round(h * ratio)
    }
    // 更新元素尺寸和 src
    board.value.updateElement(placeholderEl.id, {
      width: w,
      height: h,
      src: uploadResult.file_url,
    } as any)
    board.value.updateElementPosition(placeholderEl.id, cx - w / 2, cy - h / 2)
    // 使缓存失效，确保渲染器用新 URL 重新加载图片
    renderer.value?.invalidateImageCache(placeholderEl.id)
    saveHistory('add-image')
  }
  img.onerror = () => {
    message.error(t('Failed to load image'))
    board.value.removeElement(placeholderEl.id)
  }
  img.src = window.location.origin + uploadResult.file_url
}

// ---- Mermaid 渲染初始化 ----
;(async () => {
  const { default: mermaid } = await import('mermaid')
  mermaid.initialize({ startOnLoad: false, theme: 'default' })
})()

// ---- 页面标签 ----
const pageTabs = computed(() => pages.value.map((p, i) => ({
  label: p.name || t('Page {1}', { 1: String(i + 1) }),
  key: p.id,
  active: p.id === activePageId.value,
})))

const editingPageId = ref<string | null>(null)
const editingPageName = ref('')

function handlePageTabClick(pageId: string): void {
  setActivePage(pageId)
}

function handlePageTabDoubleClick(pageId: string, name: string): void {
  editingPageId.value = pageId
  editingPageName.value = name
  nextTick(() => pageTabInputRef.value?.focus())
}

function finishPageRename(): void {
  if (editingPageId.value) {
    renamePage(editingPageId.value, editingPageName.value)
    editingPageId.value = null
  }
}

function handleRemovePage(pageId: string, e: Event): void {
  e.stopPropagation()
  if (pages.value.length <= 1) return
  removePage(pageId)
}

function handleAddPage(): void {
  addPage()
}

// ---- 键盘快捷键 ----
function handleKeydown(e: KeyboardEvent): void {
  const target = e.target as HTMLElement
  if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') return

  // Ctrl+Z 撤销
  if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
    e.preventDefault()
    undo()
    return
  }

  // Ctrl+Shift+Z 或 Ctrl+Y 重做
  if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
    e.preventDefault()
    redo()
    return
  }

  // Delete/Backspace 删除
  if (e.key === 'Delete' || e.key === 'Backspace') {
    e.preventDefault()
    deleteSelected()
    return
  }

  // 工具快捷键
  const toolShortcuts: Record<string, ToolType> = {
    'v': 'select',
    'h': 'hand',
    'r': 'rectangle',
    'o': 'ellipse',
    'd': 'diamond',
    's': 'sticky-note',
    'a': 'arrow',
    'l': 'line',
    'p': 'freehand',
    't': 'text',
    'e': 'eraser',
  }

  if (!e.ctrlKey && !e.metaKey && toolShortcuts[e.key]) {
    setTool(toolShortcuts[e.key])
  }

  // Escape → select / cancel drawing
  if (e.key === 'Escape') {
    e.preventDefault()
    cancelDrawing()
    setSelectedIds([])
  }

  // Ctrl+A 全选
  if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
    e.preventDefault()
    setSelectedIds(board.value.activePageElements.map(el => el.id))
  }
}

// ---- 加载数据 ----
async function loadData(): Promise<void> {
  if (!recordName.value) return
  loading.value = true
  try {
    const pagetypeSlug = route.query.pagetype as string
    const pagetype = pagetypeSlug ? slugToPageType(pagetypeSlug) : 'Whiteboard'
    const result = await getRecord(pagetype, recordName.value)
    if (result.success && result.data) {
      const rec = result.data
      pageTitle.value = rec.name || rec.title || 'Whiteboard'

      let boardData: SerializedBoard | null = null
      if (rec.whiteboard_data) {
        try {
          boardData = typeof rec.whiteboard_data === 'string'
            ? JSON.parse(rec.whiteboard_data)
            : rec.whiteboard_data
        } catch (e) {
          console.error('Failed to parse whiteboard data:', e)
        }
      }

      if (canvasRef.value) {
        initCanvas(canvasRef.value, boardData)
        board.value.modified = (rec as any).modified || ''
        saveHistory('initial-load')
        if (boardData && !boardData.viewport) {
          requestAnimationFrame(() => fitView())
        }
      }
    } else {
      message.error(result.message || t('Failed to load whiteboard'))
    }
  } catch (e: any) {
    message.error(t('Failed to load') + ': ' + (e.message || t('Unknown error')))
  } finally {
    loading.value = false
  }
}

// ---- 保存数据 ----
async function saveData(): Promise<void> {
  if (!recordName.value) {
    message.warning(t('Cannot save: record name not specified'))
    return
  }
  saving.value = true
  isSaving.value = true
  try {
    const data = exportData()
    const pagetypeSlug = route.query.pagetype as string
    const pagetype = pagetypeSlug ? slugToPageType(pagetypeSlug) : 'Whiteboard'
    const result = await updateRecord(pagetype, recordName.value, {
      whiteboard_data: data
    })
    if (result.success) {
      if (result.data?.modified) {
        board.value.modified = result.data.modified
      }
      message.success(t('Saved successfully'))
    } else {
      message.error(result.message || t('Failed to save'))
    }
  } catch (e: any) {
    message.error(t('Failed to save') + ': ' + (e.message || t('Unknown error')))
  } finally {
    saving.value = false
    isSaving.value = false
  }
}

// ---- 返回 ----
function goBack(): void {
  router.back()
}

// ---- 导出 ----
async function exportAsPng(): Promise<void> {
  showExportMenu.value = false
  exporting.value = true
  try {
    await nextTick()
    const dataUrl = renderer.value?.toDataURL('image/png')
    if (!dataUrl) {
      message.error(t('Export failed'))
      return
    }
    const link = document.createElement('a')
    link.download = `${pageTitle.value || 'whiteboard'}_${Date.now()}.png`
    link.href = dataUrl
    link.click()
  } finally {
    exporting.value = false
  }
}

async function copyToClipboard(): Promise<void> {
  showExportMenu.value = false
  exporting.value = true
  try {
    await nextTick()
    const dataUrl = renderer.value?.toDataURL('image/png')
    if (!dataUrl) {
      message.error(t('Export failed'))
      return
    }

    // Convert data URL to Blob
    const res = await fetch(dataUrl)
    const blob = await res.blob()

    const clipboardItem = new ClipboardItem({ [blob.type]: blob })
    await navigator.clipboard.write([clipboardItem])
    message.success(t('Copied to clipboard.'))
  } catch (e) {
    // Fallback: copy data URL as text
    try {
      const dataUrl = renderer.value?.toDataURL('image/png')
      if (dataUrl) {
        await navigator.clipboard.writeText(dataUrl)
        message.success(t('Copied to clipboard.'))
      }
    } catch {
      message.error(t('Copy failed, please copy manually'))
    }
  } finally {
    exporting.value = false
  }
}

// ---- 文本编辑 ----
const textEditRef = ref<HTMLTextAreaElement>()

/**
 * 开始编辑文本（Excalidraw 行为）
 * - 文本工具单击：创建空文本元素 → 立即打开编辑器
 * - 双击已有文本元素：打开编辑器，选中所有文本
 * - 双击形状/便利贴：编辑其内嵌文本
 */
function startTextEdit(elementId: string): void {
  const el = board.value.getElement(elementId)
  if (!el) return

  // 只有含文本属性的元素可编辑
  if (!('text' in el) && el.type !== 'text' && el.type !== 'sticky-note'
    && el.type !== 'rectangle' && el.type !== 'ellipse' && el.type !== 'diamond') return

  editingTextId.value = elementId
  editText.value = (el as any).text || ''

  // 通知渲染器该元素正在编辑（不再在 canvas 上渲染它）
  renderer.value?.setEditingId(elementId)

  // 同步元素样式属性到 editTextStyle（与 canvas 渲染 BoardRenderer 对齐）
  const s = (el as any).style || {}
  const isTextType = el.type === 'text'
  editTextStyle.value = {
    fontSize: s.fontSize ?? (isTextType ? 20 : 14),
    fontWeight: s.fontWeight ?? '400',
    color: s.color ?? '#1e293b',
    textAlign: s.textAlign ?? (isTextType ? 'left' : 'center'),
    textBaseline: s.textBaseline ?? 'middle',
  }

  // 用渲染器 API 获取文字参数（与 canvas 渲染逻辑完全一致）
  // canvas padding=8px 对应 textarea 的水平 padding；垂直位置用 transform 精确控制
  if (renderer.value) {
    const zoom = renderer.value.getViewport().zoom
    const panX = renderer.value.getViewport().panX
    const panY = renderer.value.getViewport().panY
    const textBounds = renderer.value.getTextBoundsInElement(board.value, elementId)
    const fontSize = s.fontSize ?? (isTextType ? 20 : 14)
    const align = s.textAlign ?? (isTextType ? 'left' : 'center')
    const baseline = s.textBaseline ?? 'middle'

    const padding = 8
    const textHeight = el.height - padding * 2

    // textarea 第一行文字顶边 = fontSize（相对于 textarea 内容区顶边）
    // 根据 textBaseline 计算 transformY，使 textarea 文字与 Canvas 文字精确对齐
    // middle: 第一行基线在 textHeight/2 → transformY = (textHeight/2 - fontSize/2) * zoom
    // top:    第一行文字顶边在 textarea 顶部 → transformY = 0
    // bottom: 第一行基线在 textHeight → transformY = (textHeight - fontSize) * zoom
    let transformY: number
    if (baseline === 'top') {
      transformY = 0
    } else if (baseline === 'bottom') {
      transformY = (textHeight - fontSize) * zoom
    } else {
      transformY = (textHeight / 2 - fontSize / 2) * zoom
    }

    editTextPosition.value = {
      x: el.x * zoom + panX,
      y: el.y * zoom + panY,
      width: el.width * zoom,
      height: el.height * zoom,
    }

    // 同步水平对齐方式
    editTextStyle.value = {
      fontSize,
      fontWeight: s.fontWeight ?? '400',
      color: s.color ?? '#1e293b',
      textAlign: align,
      textBaseline: baseline,
      transformY,
    }
  }

  nextTick(() => {
    const textarea = textEditRef.value
    if (textarea) {
      textarea.focus()
      // 如果是新建的空文本，光标在末尾；否则全选
      if (editText.value) {
        textarea.select()
      }
    }
  })
}

/**
 * 完成文本编辑（Excalidraw 行为）
 * - 更新元素文本 + 自动调整尺寸
 * - 切回 select 工具
 * - 空文本则删除元素
 */
function finishTextEdit(): void {
  if (!editingTextId.value) return
  const id = editingTextId.value
  const text = editText.value.trim()

  // 先从渲染器移除编辑状态（让 canvas 停止渲染该元素）
  renderer.value?.setEditingId(null)

  if (text) {
    updateElement(id, { text } as any)
  } else {
    // 空文本 → 删除元素（Excalidraw 行为）
    board.value.removeElement(id)
  }

  editingTextId.value = null
  // 完成编辑后切回 select 工具（Excalidraw 行为）
  setTool('select')
}

/**
 * 实时同步 textarea 内容到画布元素 + 更新元素尺寸（让蓝色选择框随文字增长而扩展）
 */
function onEditTextInput(): void {
  if (!editingTextId.value) return
  const el = board.value.getElement(editingTextId.value)
  if (!el) return

  const vp = renderer.value ? renderer.value.getViewport() : { zoom: 1, panX: 0, panY: 0 }

  // 文本元素：实时计算并更新尺寸（让选择框自适应）
  if (el.type === 'text') {
    const canvas = canvasRef.value
    const ctx = canvas?.getContext('2d')
    if (ctx) {
      const fontSize = el.style.fontSize || 20
      const lineHeight = fontSize * 1.4
      ctx.font = `${el.style.fontWeight || '400'} ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`
      const padding = 16

      const lines = editText.value.split('\n')
      let maxWidth = 0
      lines.forEach(line => {
        maxWidth = Math.max(maxWidth, ctx.measureText(line).width)
      })

      const newWidth = Math.max(maxWidth + padding, 40)
      const newHeight = Math.max(lines.length * lineHeight + padding, 30)

      // 只扩展到足够容纳内容，不覆盖用户手动调大的尺寸
      const currentWidth = el.width
      const currentHeight = el.height
      const finalWidth = Math.max(newWidth, currentWidth)
      const finalHeight = Math.max(newHeight, currentHeight)

      if (finalWidth !== currentWidth || finalHeight !== currentHeight) {
        board.value.updateElementSize(editingTextId.value, finalWidth, finalHeight)
      }
    }
  }

  // 同步 textarea 位置（读取更新后的元素尺寸）
  if (renderer.value) {
    const updatedEl = board.value.getElement(editingTextId.value)
    if (updatedEl) {
      editTextPosition.value = {
        x: updatedEl.x * vp.zoom + vp.panX,
        y: updatedEl.y * vp.zoom + vp.panY,
        width: updatedEl.width * vp.zoom,
        height: updatedEl.height * vp.zoom,
      }
    }
  }
}

// ---- 属性面板事件处理 ----

/** 触发文本编辑（双击元素） */
function handleDoubleClickEdit(): void {
  const el = selectedElement.value
  if (!el) return
  startTextEdit(el.id)
}

/** 更新填充色 */
function handleFillChange(value: string): void {
  const ids = getSelectedIds()
  ids.forEach(id => updateElementStyle(id, { fill: value }))
}

/** 更新描边色 */
function handleStrokeChange(value: string): void {
  const ids = getSelectedIds()
  ids.forEach(id => {
    const el = board.value.getElement(id) as any
    if (el && (el.type === 'arrow' || el.type === 'line') && el.connectionStyle) {
      // arrow/line: 更新 connectionStyle.stroke
      updateElement(id, { connectionStyle: { ...el.connectionStyle, stroke: value } } as any)
    } else {
      // 普通形状: 更新 style.stroke
      updateElementStyle(id, { stroke: value })
    }
  })
}

/** 更新文字颜色 */
function handleTextColorChange(value: string): void {
  const ids = getSelectedIds()
  ids.forEach(id => updateElementStyle(id, { color: value }))
}

/** 更新边框宽度 */
function handleStrokeWidthChange(value: number): void {
  const ids = getSelectedIds()
  ids.forEach(id => {
    const el = board.value.getElement(id) as any
    if (el && (el.type === 'arrow' || el.type === 'line') && el.connectionStyle) {
      updateElement(id, { connectionStyle: { ...el.connectionStyle, strokeWidth: value } } as any)
    } else {
      updateElementStyle(id, { strokeWidth: value })
    }
  })
}

/** 更新透明度 */
function handleOpacityChange(value: number): void {
  const ids = getSelectedIds()
  ids.forEach(id => updateElementStyle(id, { opacity: value / 100 }))
}

/** 更新圆角 */
function handleBorderRadiusChange(value: number): void {
  const ids = getSelectedIds()
  ids.forEach(id => updateElementStyle(id, { borderRadius: value }))
}

/** 更新连线路由样式 */
function handleRouteStyleChange(value: 'straight' | 'orthogonal' | 'bezier'): void {
  const ids = getSelectedIds()
  ids.forEach(id => {
    const el = board.value.getElement(id) as any
    if (!el || !el.connectionStyle) return
    updateElement(id, {
      connectionStyle: { ...el.connectionStyle, route: value },
      waypoints: [],
    } as any)
  })
}

/** 更新终点箭头样式 */
function handleEndArrowChange(value: ArrowEnd): void {
  const ids = getSelectedIds()
  ids.forEach(id => {
    const el = board.value.getElement(id) as any
    if (!el || !el.connectionStyle) return
    updateElement(id, { connectionStyle: { ...el.connectionStyle, endArrow: value } } as any)
  })
}

/** 更新 X 坐标 */
function handleXChange(value: number | null): void {
  if (value === null) return
  const ids = getSelectedIds()
  ids.forEach(id => updateElementPosition(id, value, board.value.getElement(id)?.y ?? 0))
}

/** 更新 Y 坐标 */
function handleYChange(value: number | null): void {
  if (value === null) return
  const ids = getSelectedIds()
  ids.forEach(id => updateElementPosition(id, board.value.getElement(id)?.x ?? 0, value))
}

/** 更新宽度 */
function handleWidthChange(value: number | null): void {
  if (value === null || value < 1) return
  const ids = getSelectedIds()
  ids.forEach(id => updateElementSize(id, value, board.value.getElement(id)?.height ?? 0))
}

/** 更新高度 */
function handleHeightChange(value: number | null): void {
  if (value === null || value < 1) return
  const ids = getSelectedIds()
  ids.forEach(id => updateElementSize(id, board.value.getElement(id)?.width ?? 0, value))
}

/** 更新字体大小 */
function handleFontSizeChange(value: number | null): void {
  if (value === null) return
  const ids = getSelectedIds()
  ids.forEach(id => updateElementStyle(id, { fontSize: value }))
}

/** 更新字体粗细 */
function handleFontWeightChange(value: string): void {
  const ids = getSelectedIds()
  ids.forEach(id => updateElementStyle(id, { fontWeight: value }))
}

/** 更新线条风格 */
function handleLineStyleChange(value: LineStyle): void {
  const ids = getSelectedIds()
  ids.forEach(id => {
    const el = board.value.getElement(id) as any
    if (el && (el.type === 'arrow' || el.type === 'line') && el.connectionStyle) {
      updateElement(id, { connectionStyle: { ...el.connectionStyle, lineStyle: value } } as any)
    } else {
      updateElementStyle(id, { lineStyle: value })
    }
  })
}

/** 更新文字水平对齐 */
function handleTextAlignChange(value: TextAlign): void {
  const ids = getSelectedIds()
  ids.forEach(id => updateElementStyle(id, { textAlign: value }))
}

/** 更新文字垂直对齐 */
function handleTextBaselineChange(value: TextBaseline): void {
  const ids = getSelectedIds()
  ids.forEach(id => updateElementStyle(id, { textBaseline: value }))
}

/** 移动到最前 */
function handleBringToFront(): void {
  const ids = getSelectedIds()
  ids.forEach(id => bringToFront(id))
}

/** 移动到最后 */
function handleSendToBack(): void {
  const ids = getSelectedIds()
  ids.forEach(id => sendToBack(id))
}

/** 复制元素 */
function handleDuplicate(): void {
  const ids = getSelectedIds()
  ids.forEach(id => {
    const el = board.value.getElement(id)
    if (!el) return
    addElement(el.type, {
      x: el.x + 20,
      y: el.y + 20,
      width: el.width,
      height: el.height,
      style: { ...el.style },
      text: (el as any).text,
    } as any)
  })
}

/** 删除元素 */
function handleDelete(): void {
  deleteSelected()
}

/** 锁定/解锁元素 */
function handleLockToggle(): void {
  const ids = getSelectedIds()
  ids.forEach(id => {
    const el = board.value.getElement(id)
    if (!el) return
    updateElement(id, { locked: !el.locked } as any)
  })
}

// ---- 生命周期 ----
onMounted(() => {
  document.addEventListener('keydown', handleKeydown)
  document.addEventListener('fullscreenchange', handleFullscreenChange)
  document.addEventListener('webkitfullscreenchange', handleFullscreenChange)

  if (canvasRef.value) {
    if (recordName.value) {
      loadData().then(() => {
        if (recordName.value) {
          realtime.watchDocument('Whiteboard', recordName.value, { onUpdate: handlePgUpdate })
        }
      })
    } else {
      initCanvas(canvasRef.value)
      saveHistory('initial')
    }
  }

  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  document.removeEventListener('keydown', handleKeydown)
  document.removeEventListener('fullscreenchange', handleFullscreenChange)
  document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.removeEventListener('resize', handleResize)
})

function handleResize(): void {
  resize()
}
</script>

<template>
  <div
    class="whiteboard-editor"
    :class="{ 'fullscreen': isFullscreen }"
    ref="editorRoot"
  >
    <!-- 顶部工具栏 -->
    <div class="toolbar">
      <div class="toolbar-left">
        <button class="toolbar-btn back-btn" @click="goBack" :title="t('Back')">
          <n-icon size="18"><Icon icon="tabler:arrow-left" /></n-icon>
        </button>
        <span class="page-title">{{ pageTitle }}</span>
        <div class="toolbar-divider"></div>
        <button
          class="toolbar-btn"
          :disabled="!canUndo"
          @click="undo"
          :title="t('Undo') + ' (Ctrl+Z)'"
        >
          <n-icon size="16"><Icon icon="tabler:arrow-back-up" /></n-icon>
        </button>
        <button
          class="toolbar-btn"
          :disabled="!canRedo"
          @click="redo"
          :title="t('Redo') + ' (Ctrl+Y)'"
        >
          <n-icon size="16"><Icon icon="tabler:arrow-forward-up" /></n-icon>
        </button>
      </div>

      <div class="toolbar-center">
        <!-- 工具按钮 -->
        <div class="tool-group" v-for="group in toolGroups" :key="group.label">
          <button
            v-for="item in group.items"
            :key="item.key"
            :class="['tool-btn', { active: currentTool === item.key }]"
            @click="handleToolClick(item.key)"
            :title="item.label"
          >
            <n-icon size="18">
              <svg v-if="item.icon === '◇'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" xmlns="http://www.w3.org/2000/svg">
                <polygon points="12,2 22,12 12,22 2,12"/>
              </svg>
              <Icon v-else :icon="item.icon" />
            </n-icon>
          </button>
          <div class="tool-divider"></div>
        </div>
      </div>

      <div class="toolbar-right">
        <!-- 缩放控制（截图样式分组） -->
        <div class="zoom-group">
          <button class="zoom-btn" @click="zoomOut" :title="t('Zoom Out')">
            <n-icon size="14"><Icon icon="tabler:minus" /></n-icon>
          </button>
          <button class="zoom-btn zoom-reset" @click="resetZoom" :title="t('Reset Zoom')">
            {{ Math.round(currentZoom * 100) }}%
          </button>
          <button class="zoom-btn" @click="zoomIn" :title="t('Zoom In')">
            <n-icon size="14"><Icon icon="tabler:plus" /></n-icon>
          </button>
        </div>
        <div class="toolbar-divider"></div>
        <button class="toolbar-btn" @click="fitView" :title="t('Fit View')">
          <n-icon size="16"><Icon icon="tabler:arrows-maximize" /></n-icon>
        </button>
        <button
          class="toolbar-btn"
          @click="toggleFullscreen"
          :title="isFullscreen ? t('Exit Fullscreen') : t('Fullscreen')"
        >
          <n-icon size="16"><Icon :icon="isFullscreen ? 'tabler:arrows-minimize' : 'tabler:maximize'" /></n-icon>
        </button>
        <div class="toolbar-divider"></div>
        <n-button
          size="small"
          class="save-btn-brand"
          :loading="saving"
          :disabled="saving"
          :title="t('Save')"
          @click="saveData"
        >
          <template #icon>
            <n-icon><Icon icon="tabler:check" /></n-icon>
          </template>
          <span class="btn-text">{{ saving ? t('Saving...') : t('Save') }}</span>
        </n-button>
        <n-popover
          v-model:show="showExportMenu"
          trigger="click"
          placement="bottom-end"
          :show-arrow="false"
        >
          <template #trigger>
            <button class="toolbar-btn" :disabled="exporting" :title="t('Export')">
              <n-icon size="16">
                <Icon v-if="!exporting" icon="tabler:dots" />
                <Icon v-else icon="tabler:loader" class="spin" />
              </n-icon>
            </button>
          </template>
          <div class="export-menu">
            <button class="export-menu-item" @click="openMermaidModal">
              <n-icon size="14"><Icon icon="tabler:chart-dots" /></n-icon>
              <span>{{ t('Import Mermaid') }}</span>
            </button>
            <div class="export-menu-divider"></div>
            <button class="export-menu-item" @click="exportAsPng">
              <n-icon size="14"><Icon icon="tabler:photo" /></n-icon>
              <span>{{ t('Export as PNG') }}</span>
            </button>
            <button class="export-menu-item" @click="copyToClipboard">
              <n-icon size="14"><Icon icon="tabler:copy" /></n-icon>
              <span>{{ t('Copy to Clipboard') }}</span>
            </button>
          </div>
        </n-popover>
      </div>
    </div>

    <!-- 加载遮罩 -->
    <div v-if="loading" class="whiteboard-loading">
      <i class="fa fa-spinner fa-spin"></i>
      <p>{{ t('Loading whiteboard...') }}</p>
    </div>

    <!-- Canvas 画布 -->
    <div
      class="whiteboard-canvas"
      :class="{
        'canvas-eraser-mode': currentTool === 'eraser' && !eraserHoverElement,
        'canvas-eraser-hover': currentTool === 'eraser' && eraserHoverElement
      }"
    >
      <input
        ref="imageFileInputRef"
        type="file"
        accept="image/*"
        style="display:none"
        @change="handleImageFileSelect"
      />
      <canvas ref="canvasRef" class="whiteboard-canvas-el"></canvas>
      <!-- 橡皮擦悬停高亮 -->
      <div
        v-if="eraserHoverElement && currentTool === 'eraser'"
        class="eraser-hover-highlight"
        :style="{
          left: (eraserHoverElement.x * (renderer?.getViewport().zoom || 1) + (renderer?.getViewport().panX || 0)) + 'px',
          top: (eraserHoverElement.y * (renderer?.getViewport().zoom || 1) + (renderer?.getViewport().panY || 0)) + 'px',
          width: (eraserHoverElement.width * (renderer?.getViewport().zoom || 1)) + 'px',
          height: (eraserHoverElement.height * (renderer?.getViewport().zoom || 1)) + 'px',
        }"
      ></div>
      <!-- 文本编辑覆盖层：wrapper 实现垂直居中，textarea 负责文本输入 -->
      <!-- 样式与 canvas 渲染 BoardRenderer 完全对齐，确保所见即所得 -->
      <div
        v-if="editingTextId"
        class="text-edit-overlay-wrap"
        :style="{
          left: editTextPosition.x + 'px',
          top: editTextPosition.y + 'px',
          width: editTextPosition.width + 'px',
          height: editTextPosition.height + 'px',
        }"
      >
        <textarea
          ref="textEditRef"
          class="text-edit-overlay"
          :style="{
            fontSize: (editTextStyle.fontSize * (renderer?.getViewport().zoom || 1)) + 'px',
            fontWeight: editTextStyle.fontWeight,
            color: editTextStyle.color,
            textAlign: editTextStyle.textAlign,
            lineHeight: 1.4,
            transform: 'translateY(' + (editTextStyle.transformY || 0) + 'px)',
          }"
          v-model="editText"
          @input="onEditTextInput"
          @blur="finishTextEdit"
          @keydown.escape="finishTextEdit"
        ></textarea>
      </div>
    </div>

    <!-- 文本编辑覆盖层占位（已移入 canvas 容器） -->

    <!-- 底部页面标签 -->
    <div class="page-tabs">
      <div
        v-for="tab in pageTabs"
        :key="tab.key"
        :class="['page-tab', { active: tab.active }]"
        @click="handlePageTabClick(tab.key)"
        @dblclick="handlePageTabDoubleClick(tab.key, tab.label)"
      >
        <input
          v-if="editingPageId === tab.key"
          v-model="editingPageName"
          class="page-tab-input"
          @blur="finishPageRename"
          @keydown.enter="finishPageRename"
          @keydown.escape="editingPageId = null"
          @click.stop
          ref="pageTabInputRef"
        />
        <span v-else class="page-tab-label">{{ tab.label }}</span>
        <button
          v-if="pages.length > 1"
          class="page-tab-remove"
          @click="handleRemovePage(tab.key, $event)"
          :title="t('Remove Page')"
        >
          <n-icon size="10"><Icon icon="tabler:x" /></n-icon>
        </button>
      </div>
      <button class="page-tab-add" @click="handleAddPage" :title="t('Add Page')">
        <n-icon size="14"><Icon icon="tabler:plus" /></n-icon>
      </button>
    </div>

    <!-- 右侧属性面板 -->
    <div class="properties-panel" v-if="selectedIds.length > 0">
      <!-- 面板头部 -->
      <div class="panel-header">
        <span>{{ t('Properties') }}</span>
        <n-tooltip v-if="selectedIds.length === 1 && selectedElement" trigger="hover">
          <template #trigger>
            <button class="panel-type-badge">
              <Icon :icon="elementTypeIcon" size="12" />
              {{ elementTypeLabel }}
            </button>
          </template>
          {{ t('Element type') }}
        </n-tooltip>
        <span v-else-if="selectedIds.length > 1" class="panel-multi-badge">
          {{ selectedIds.length }} {{ t('selected') }}
        </span>
      </div>

      <div class="panel-content">
        <!-- ========== 位置 & 尺寸 ========== -->
        <template v-if="selectedIds.length === 1">
          <div class="panel-section">
            <div class="panel-section-title">{{ t('Position & Size') }}</div>
            <!-- X / Y 坐标 -->
            <div class="prop-row-2col">
              <div class="prop-field">
                <label>X</label>
                <n-input-number
                  size="tiny"
                  :value="Math.round(selectedElement?.x ?? 0)"
                  @update:value="handleXChange"
                  :show-button="false"
                  placeholder="X"
                />
              </div>
              <div class="prop-field">
                <label>Y</label>
                <n-input-number
                  size="tiny"
                  :value="Math.round(selectedElement?.y ?? 0)"
                  @update:value="handleYChange"
                  :show-button="false"
                  placeholder="Y"
                />
              </div>
            </div>
            <!-- Width / Height -->
            <div class="prop-row-2col">
              <div class="prop-field">
                <label>W</label>
                <n-input-number
                  size="tiny"
                  :value="Math.round(selectedElement?.width ?? 0)"
                  @update:value="handleWidthChange"
                  :min="1"
                  :show-button="false"
                  placeholder="W"
                />
              </div>
              <div class="prop-field">
                <label>H</label>
                <n-input-number
                  size="tiny"
                  :value="Math.round(selectedElement?.height ?? 0)"
                  @update:value="handleHeightChange"
                  :min="1"
                  :show-button="false"
                  placeholder="H"
                />
              </div>
            </div>
          </div>

          <!-- ========== 填充 ========== -->
          <template v-if="hasFillControl">
            <div class="panel-section">
              <div class="panel-section-title">{{ t('Fill') }}</div>
              <div class="color-row">
                <div class="color-preview" :style="{ background: selectedElement?.style?.fill || '#ffffff' }"></div>
                <n-color-picker
                  size="small"
                  :value="selectedElement?.style?.fill || '#ffffff'"
                  @update:value="handleFillChange"
                  :swatches="fillSwatches"
                  :show-alpha="false"
                  :modes="['hex']"
                />
                <n-tooltip trigger="hover">
                  <template #trigger>
                    <button class="color-btn" @click="handleFillChange('#ffffff')" title="Reset">
                      <Icon icon="tabler:x" size="12" />
                    </button>
                  </template>
                  Remove fill
                </n-tooltip>
              </div>
            </div>
          </template>

          <!-- ========== 描边 ========== -->
          <template v-if="hasStrokeControl">
            <div class="panel-section">
              <div class="panel-section-title">{{ t('Stroke') }}</div>
              <div class="color-row">
                <div class="color-preview" :style="{ background: currentStroke }"></div>
                <n-color-picker
                  size="small"
                  :value="currentStroke"
                  @update:value="handleStrokeChange"
                  :swatches="strokeSwatches"
                  :show-alpha="false"
                  :modes="['hex']"
                />
              </div>
              <!-- 描边宽度 -->
              <div class="prop-row" v-if="hasStrokeWidthControl">
                <label>{{ t('Stroke Width') }}</label>
                <n-select
                  size="tiny"
                  :value="currentStrokeWidth"
                  @update:value="handleStrokeWidthChange"
                  :options="strokeWidthOptions"
                  style="width: 72px;"
                />
              </div>
              <!-- 边框样式 -->
              <div class="prop-row" style="margin-top: 6px;">
                <label>{{ t('Border Style') }}</label>
                <div class="line-style-picker">
                  <button
                    v-for="opt in lineStyleOptions"
                    :key="opt.value"
                    :class="['line-style-btn', { active: currentLineStyle === opt.value }]"
                    @click="handleLineStyleChange(opt.value)"
                    :title="opt.label"
                    v-html="opt.icon"
                  />
                </div>
              </div>
            </div>
          </template>

          <!-- ========== 圆角 ========== -->
          <template v-if="hasBorderRadiusControl">
            <div class="panel-section">
              <div class="panel-section-title">{{ t('Corner Radius') }}</div>
              <div class="radius-presets">
                <button
                  v-for="p in borderRadiusPresets"
                  :key="p.value"
                  :class="['radius-preset-btn', { active: (selectedElement?.style?.borderRadius ?? 4) === p.value }]"
                  @click="handleBorderRadiusChange(p.value)"
                  :title="p.label === '9999' ? 'Fully rounded' : p.label + 'px'"
                >
                  <span v-if="p.value === 0">□</span>
                  <span v-else-if="p.value === 9999" style="font-size:8px">○</span>
                  <span v-else>{{ p.label }}</span>
                </button>
              </div>
            </div>
          </template>

          <!-- ========== 箭头控制 ========== -->
          <template v-if="hasArrowControl">
            <div class="panel-section">
              <!-- 连线样式（路由） -->
              <div class="panel-section-title">{{ t('Arrow Style') }}</div>
              <div class="prop-row">
                <n-select
                  size="tiny"
                  :value="currentRouteStyle"
                  @update:value="handleRouteStyleChange"
                  :options="[
                    { label: t('Straight'), value: 'straight' },
                    { label: t('Orthogonal'), value: 'orthogonal' },
                    { label: t('Bezier'), value: 'bezier' },
                  ]"
                  style="width: 100%;"
                />
              </div>
              <!-- 终点箭头 -->
              <div class="panel-section-title" style="margin-top: 10px;">{{ t('End Arrow') }}</div>
              <div class="arrow-end-picker">
                <button
                  v-for="opt in arrowEndOptions"
                  :key="opt.value"
                  :class="['arrow-end-btn', { active: currentEndArrow === opt.value }]"
                  @click="handleEndArrowChange(opt.value)"
                  :title="opt.label"
                  v-html="opt.icon"
                />
              </div>
            </div>
          </template>

          <!-- ========== 透明度 ========== -->
          <div class="panel-section">
            <div class="panel-section-title">{{ t('Opacity') }}</div>
            <div class="prop-row">
              <n-slider
                :value="Math.round((selectedElement?.style?.opacity ?? 1) * 100)"
                @update:value="handleOpacityChange"
                :step="5"
                :min="0"
                :max="100"
                :marks="opacityMarks"
                style="flex:1"
              />
              <span class="opacity-value">
                {{ Math.round((selectedElement?.style?.opacity ?? 1) * 100) }}%
              </span>
            </div>
          </div>

          <!-- ========== 文字样式 ========== -->
          <template v-if="hasTextControl">
            <div class="panel-section">
              <div class="panel-section-title">{{ t('Text') }}</div>
              <!-- 编辑按钮 -->
              <n-button size="tiny" block style="margin-bottom: 6px;" @click="handleDoubleClickEdit">
                <template #icon><Icon icon="tabler:text-size" size="12" /></template>
                {{ t('Edit Text') }}
              </n-button>
              <!-- 文字颜色 -->
              <div class="color-row">
                <div class="color-preview" :style="{ background: selectedElement?.style?.color || '#1e293b' }"></div>
                <n-color-picker
                  size="small"
                  :value="selectedElement?.style?.color || '#1e293b'"
                  @update:value="handleTextColorChange"
                  :swatches="strokeSwatches"
                  :show-alpha="false"
                  :modes="['hex']"
                />
              </div>
              <!-- 字号 -->
              <div class="prop-row">
                <label>{{ t('Font Size') }}</label>
                <n-select
                  size="tiny"
                  :value="selectedElement?.style?.fontSize ?? 14"
                  @update:value="handleFontSizeChange"
                  :options="[
                    { label: '10', value: 10 },
                    { label: '12', value: 12 },
                    { label: '14', value: 14 },
                    { label: '16', value: 16 },
                    { label: '18', value: 18 },
                    { label: '20', value: 20 },
                    { label: '24', value: 24 },
                    { label: '28', value: 28 },
                    { label: '32', value: 32 },
                    { label: '40', value: 40 },
                    { label: '48', value: 48 },
                    { label: '64', value: 64 },
                  ]"
                  :filterable="true"
                  :allow-input="true"
                  style="width: 80px;"
                />
              </div>
              <!-- 字重 -->
              <div class="prop-row">
                <label>{{ t('Font Weight') }}</label>
                <n-select
                  size="tiny"
                  :value="selectedElement?.style?.fontWeight || '400'"
                  @update:value="handleFontWeightChange"
                  :options="[
                    { label: t('Light', undefined, 'Whiteboard'), value: '300' },
                    { label: t('Regular', undefined, 'Whiteboard'), value: '400' },
                    { label: t('Medium', undefined, 'Whiteboard'), value: '500' },
                    { label: t('Bold', undefined, 'Whiteboard'), value: '700' },
                  ]"
                  style="width: 90px;"
                />
              </div>
              <!-- 文字水平对齐 -->
              <div class="prop-row">
                <label>{{ t('Align') }}</label>
                <div class="align-picker">
                  <button
                    v-for="opt in textAlignOptions"
                    :key="opt.value"
                    :class="['align-btn', { active: currentTextAlign === opt.value }]"
                    @click="handleTextAlignChange(opt.value)"
                    :title="opt.label"
                    v-html="opt.icon"
                  />
                </div>
              </div>
              <!-- 文字垂直对齐 -->
              <div class="prop-row">
                <label>{{ t('Vertical Align') }}</label>
                <div class="align-picker">
                  <button
                    v-for="opt in textBaselineOptions"
                    :key="opt.value"
                    :class="['align-btn', { active: currentTextBaseline === opt.value }]"
                    @click="handleTextBaselineChange(opt.value)"
                    :title="opt.label"
                    v-html="opt.icon"
                  />
                </div>
              </div>
            </div>
          </template>
        </template>

        <!-- ========== 多选时的批量操作 ========== -->
        <template v-else-if="selectedIds.length > 1">
          <div class="panel-section">
            <div class="panel-section-title">{{ t('Batch Edit') }} ({{ selectedIds.length }})</div>
            <!-- 批量填充色 -->
            <div class="prop-row">
              <label>{{ t('Fill') }}</label>
              <n-color-picker
                size="small"
                @update:value="handleFillChange"
                :swatches="fillSwatches"
                :show-alpha="false"
                :modes="['hex']"
              />
            </div>
            <!-- 批量描边色 -->
            <div class="prop-row">
              <label>{{ t('Stroke') }}</label>
              <n-color-picker
                size="small"
                @update:value="handleStrokeChange"
                :swatches="strokeSwatches"
                :show-alpha="false"
                :modes="['hex']"
              />
            </div>
            <!-- 批量描边宽度 -->
            <div class="prop-row">
              <label>{{ t('Stroke Width') }}</label>
              <n-select
                size="tiny"
                @update:value="handleStrokeWidthChange"
                :options="strokeWidthOptions"
                style="width: 72px;"
              />
            </div>
            <!-- 批量透明度 -->
            <div class="prop-row">
              <label>{{ t('Opacity') }}</label>
              <n-slider
                :value="100"
                @update:value="handleOpacityChange"
                :step="5"
                :min="0"
                :max="100"
                style="flex:1"
              />
            </div>
          </div>
        </template>

        <!-- ========== 图层操作 ========== -->
        <div class="panel-section">
          <div class="panel-section-title">{{ t('Layer') }}</div>
          <div class="layer-buttons">
            <n-tooltip trigger="hover"><template #trigger>
              <n-button size="tiny" @click="handleBringToFront" :disabled="selectedIds.length === 0">
                <template #icon><Icon icon="tabler:arrow-up" size="12" /></template>
              </n-button>
            </template>{{ t('Bring to Front') }}</n-tooltip>
            <n-tooltip trigger="hover"><template #trigger>
              <n-button size="tiny" @click="handleSendToBack" :disabled="selectedIds.length === 0">
                <template #icon><Icon icon="tabler:arrow-down" size="12" /></template>
              </n-button>
            </template>{{ t('Send to Back') }}</n-tooltip>
            <n-tooltip trigger="hover"><template #trigger>
              <n-button size="tiny" @click="handleDuplicate" :disabled="selectedIds.length === 0">
                <template #icon><Icon icon="tabler:copy" size="12" /></template>
              </n-button>
            </template>{{ t('Duplicate') }}</n-tooltip>
            <n-tooltip trigger="hover"><template #trigger>
              <n-button size="tiny" @click="handleLockToggle" :disabled="selectedIds.length === 0">
                <template #icon><Icon :icon="selectedElement?.locked ? 'tabler:lock' : 'tabler:lock-open'" size="12" /></template>
              </n-button>
            </template>{{ selectedElement?.locked ? t('Unlock') : t('Lock') }}</n-tooltip>
          </div>
        </div>

        <!-- ========== 删除 ========== -->
        <div class="panel-section danger-section">
          <n-button size="small" type="error" block @click="handleDelete" :disabled="selectedIds.length === 0">
            <template #icon><Icon icon="tabler:trash" size="14" /></template>
            {{ t('Delete') }}
          </n-button>
        </div>
      </div>
    </div>

    <!-- Mermaid 导入弹窗 -->
    <n-modal
      v-model:show="showMermaidModal"
      preset="card"
      :title="t('Import Mermaid')"
      :style="{ width: '860px', maxWidth: '95vw' }"
      :mask-closable="false"
    >
      <div class="mermaid-modal-body">
        <div class="mermaid-modal-left">
          <div class="mermaid-modal-left-header">
            <span>{{ t('Mermaid Source') }}</span>
            <button class="mermaid-upload-btn" @click="mermaidFileInputRef?.click()">
              <n-icon size="14"><Icon icon="tabler:upload" /></n-icon>
              <span>{{ t('Upload .mmd File') }}</span>
            </button>
            <input
              ref="mermaidFileInputRef"
              type="file"
              accept=".mmd,.md,text/markdown"
              style="display:none"
              @change="handleMermaidFileSelect"
            />
          </div>
          <textarea
            v-model="mermaidCode"
            class="mermaid-code-input"
            :placeholder="t('Enter Mermaid diagram code here...')"
            spellcheck="false"
            @input="renderMermaidPreview"
          ></textarea>
        </div>
        <div class="mermaid-modal-right">
          <div class="mermaid-modal-right-header">
            <span>{{ t('Preview') }}</span>
          </div>
          <div class="mermaid-preview-area">
            <div v-if="mermaidPreviewError" class="mermaid-error">{{ mermaidPreviewError }}</div>
            <div v-else-if="mermaidPreview" v-html="mermaidPreview"></div>
            <div v-else class="mermaid-preview-placeholder">{{ t('Preview will appear here...') }}</div>
          </div>
        </div>
      </div>
      <template #footer>
        <div class="mermaid-modal-footer">
          <n-button @click="showMermaidModal = false">{{ t('Cancel') }}</n-button>
          <n-button
            type="primary"
            :disabled="!mermaidCode.trim()"
            :loading="mermaidInserting"
            @click="insertMermaidFromModal"
          >
            {{ t('Insert to Canvas') }}
          </n-button>
        </div>
      </template>
    </n-modal>
  </div>
</template>

<style scoped>
.whiteboard-editor {
  display: flex;
  flex-direction: column;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: #f8fafc;
  overflow: hidden;
}

/* ===== 工具栏 ===== */
.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 12px;
  background: #ffffff;
  border-bottom: 1px solid #e2e8f0;
  gap: 12px;
  z-index: 10;
  flex-shrink: 0;
  min-height: 44px;
}

.toolbar-left,
.toolbar-right {
  display: flex;
  align-items: center;
  gap: 4px;
}

.toolbar-center {
  display: flex;
  align-items: center;
  gap: 2px;
}

.toolbar-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  background: transparent;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  color: #64748b;
  transition: all 0.15s;
}

.toolbar-btn:hover {
  background: #f1f5f9;
  color: #334155;
}

.toolbar-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.toolbar-btn.active,
.tool-btn.active {
  background: #dbeafe;
  color: #2563eb;
}

.back-btn {
  margin-right: 4px;
}

.page-title {
  font-size: 14px;
  font-weight: 500;
  color: #1e293b;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.toolbar-divider {
  width: 1px;
  height: 20px;
  background: #e2e8f0;
  margin: 0 6px;
}

.tool-group {
  display: flex;
  align-items: center;
  gap: 2px;
}

.tool-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  background: transparent;
  border: 2px solid transparent;
  border-radius: 8px;
  cursor: pointer;
  color: #64748b;
  transition: all 0.15s;
}

.tool-btn:hover {
  background: #f1f5f9;
  color: #334155;
}

.tool-btn.active {
  background: #dbeafe;
  border-color: #3b82f6;
  color: #2563eb;
}

.tool-divider {
  width: 1px;
  height: 24px;
  background: #e2e8f0;
  margin: 0 4px;
}

.save-btn-brand {
  background: #e6f8f0 !important;
  border: 1px solid #1fc76f !important;
  color: #0d684b !important;
}

.save-btn-brand :deep(.n-button__border) {
  border: none !important;
  border-color: transparent !important;
}

.save-btn-brand :deep(.n-button__state-border) {
  border: none !important;
  border-color: transparent !important;
}

.save-btn-brand:hover {
  background: #dcfce7 !important;
  border-color: #1fc76f !important;
  border: 1px solid #1fc76f !important;
  color: #166534 !important;
  box-shadow: 0 2px 8px rgba(31, 199, 111, 0.15) !important;
}

.save-btn-brand:hover :deep(.n-button__border),
.save-btn-brand:hover :deep(.n-button__state-border) {
  border: none !important;
  border-color: transparent !important;
}

.save-btn-brand:active {
  background: #1fc76f !important;
  border-color: #1fc76f !important;
  border: 1px solid #1fc76f !important;
  color: white !important;
  box-shadow: 0 1px 4px rgba(31, 199, 111, 0.2) !important;
}

.save-btn-brand:active :deep(.n-button__border),
.save-btn-brand:active :deep(.n-button__state-border) {
  border: none !important;
  border-color: transparent !important;
}

.fullscreen {
  position: fixed !important;
  top: 0 !important;
  left: 0 !important;
  right: 0 !important;
  bottom: 0 !important;
  width: 100vw !important;
  height: 100vh !important;
  z-index: 99999;
}

/* ===== 截图样式缩放分组 ===== */
.zoom-group {
  display: flex;
  align-items: center;
  background: #f1f5f9;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  overflow: hidden;
  padding: 2px;
  gap: 1px;
}

.zoom-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 26px;
  background: transparent;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  color: #64748b;
  transition: all 0.15s;
}

.zoom-btn:hover {
  background: #e2e8f0;
  color: #334155;
}

.zoom-btn:active {
  background: #dbeafe;
  color: #2563eb;
}

.zoom-reset {
  border-left: 1px solid #e2e8f0;
  border-right: 1px solid #e2e8f0;
}

.btn-text {
  font-size: 12px;
}

/* ===== 橡皮擦悬停高亮 ===== */
.eraser-hover-highlight {
  position: absolute;
  pointer-events: none;
  background: rgba(239, 68, 68, 0.08);
  border: 2px solid rgba(239, 68, 68, 0.6);
  border-radius: 6px;
  transition: opacity 0.15s;
}

/* ===== 橡皮擦自定义光标 ===== */
/* 现代简约橡皮擦光标 - 类似 Figma/Excalidraw 风格 */
.canvas-eraser-mode {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'%3E%3Crect x='3' y='3' width='18' height='18' rx='3' fill='%23ffffff' stroke='%2364748b' stroke-width='1.5'/%3E%3Cpath d='M9 12h6M9 8h4M9 16h3' stroke='%2364748b' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E") 10 10, auto;
}

/* 悬停到可删除元素上时的光标 */
.canvas-eraser-hover {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'%3E%3Crect x='3' y='3' width='18' height='18' rx='3' fill='%23fef2f2' stroke='%23ef4444' stroke-width='1.5'/%3E%3Cpath d='M9 12h6M9 8h4M9 16h3' stroke='%23ef4444' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E") 10 10, auto;
}

/* ===== Canvas ===== */
.whiteboard-canvas {
  flex: 1;
  position: relative;
  overflow: hidden;
}

.whiteboard-canvas-el {
  display: block;
  width: 100%;
  height: 100%;
}

/* ===== 加载 ===== */
.whiteboard-loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  color: #64748b;
  z-index: 5;
}

.whiteboard-loading i {
  font-size: 32px;
  margin-bottom: 8px;
}

/* ===== 文本编辑 ===== */
.text-edit-overlay-wrap {
  position: absolute;
  z-index: 20;
  box-sizing: border-box;
  overflow: hidden;
  border: none;
  outline: none;
}
/* textarea：绝对定位填满 wrapper 内容区（无 padding，transform 精确控制文字垂直位置）
   样式由 inline :style 同步 BoardRenderer */
.text-edit-overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  padding: 0 8px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  border: none;
  border-radius: 0;
  background: transparent;
  outline: none;
  resize: none;
  overflow: hidden;
  caret-color: #3b82f6;
  box-sizing: border-box;
  word-break: break-word;
  white-space: pre-wrap;
}

/* ===== 页面标签 ===== */
.page-tabs {
  display: flex;
  align-items: center;
  padding: 4px 12px;
  background: #ffffff;
  border-top: 1px solid #e2e8f0;
  gap: 4px;
  z-index: 10;
  flex-shrink: 0;
}

.page-tab {
  position: relative;
  padding: 4px 24px 4px 12px;
  font-size: 12px;
  color: #64748b;
  background: #f1f5f9;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.15s;
}

.page-tab:hover {
  background: #e2e8f0;
  color: #334155;
}

.page-tab.active {
  background: #dbeafe;
  color: #2563eb;
  font-weight: 500;
}

.page-tab-label {
  max-width: 100px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.page-tab-input {
  background: transparent;
  border: none;
  outline: none;
  font-size: 12px;
  font-family: inherit;
  color: inherit;
  width: 80px;
  padding: 0;
}

.page-tab-remove {
  position: absolute;
  top: 50%;
  right: 4px;
  transform: translateY(-50%);
  display: none;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: none;
  border-radius: 4px;
  background: transparent;
  color: #94a3b8;
  cursor: pointer;
  transition: all 0.15s;
}

.page-tab:hover .page-tab-remove {
  display: flex;
}

.page-tab-remove:hover {
  background: #fee2e2;
  color: #ef4444;
}

.page-tab-add {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  background: transparent;
  border: 1px dashed #cbd5e1;
  border-radius: 4px;
  cursor: pointer;
  color: #94a3b8;
  transition: all 0.15s;
}

.page-tab-add:hover {
  background: #f1f5f9;
  color: #64748b;
  border-color: #94a3b8;
}

/* ===== 属性面板 ===== */
.properties-panel {
  position: absolute;
  right: 12px;
  top: 56px;
  width: 224px;
  background: #ffffff;
  border: 1px solid #e2e8f0;
  border-radius: 10px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
  z-index: 10;
  max-height: calc(100vh - 120px);
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: #cbd5e1 transparent;
  padding: 12px 0;
}

.properties-panel::-webkit-scrollbar {
  width: 4px;
}
.properties-panel::-webkit-scrollbar-track {
  background: transparent;
}
.properties-panel::-webkit-scrollbar-thumb {
  background: #cbd5e1;
  border-radius: 2px;
}

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  font-size: 12px;
  font-weight: 600;
  color: #475569;
  border-bottom: 1px solid #f1f5f9;
  background: #fafbfc;
}

.panel-type-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px;
  font-size: 11px;
  font-weight: 500;
  color: #64748b;
  background: #f1f5f9;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  cursor: default;
  text-transform: capitalize;
}

.panel-multi-badge {
  font-size: 11px;
  font-weight: 500;
  color: #64748b;
  background: #f1f5f9;
  padding: 2px 8px;
  border-radius: 4px;
}

.panel-content {
  padding: 8px 14px 20px;
  display: flex;
  flex-direction: column;
  gap: 0;
}

.panel-section {
  padding: 0;
  border-bottom: 1px solid #f8fafc;
}
.panel-section:last-child {
  border-bottom: none;
}

.panel-section-title {
  font-size: 10px;
  font-weight: 600;
  color: #94a3b8;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  margin-bottom: 6px;
  padding: 8px 0 4px;
}

/* 行 */
.prop-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 4px 0;
}
.prop-row:last-child {
  padding-bottom: 0;
}

.prop-row label {
  font-size: 12px;
  color: #64748b;
  white-space: nowrap;
  flex-shrink: 0;
  min-width: 52px;
}

/* 双列行 */
.prop-row-2col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  padding: 4px 0;
}

.prop-field {
  display: flex;
  align-items: center;
  gap: 4px;
}

.prop-field label {
  font-size: 11px;
  color: #94a3b8;
  width: 14px;
  flex-shrink: 0;
}

/* 颜色行 */
.color-row {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}

.color-preview {
  width: 20px;
  height: 20px;
  border-radius: 4px;
  border: 1px solid #e2e8f0;
  flex-shrink: 0;
  box-shadow: inset 0 0 0 1px rgba(0,0,0,0.05);
}

.color-btn {
  width: 20px;
  height: 20px;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  background: #f8fafc;
  color: #94a3b8;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.15s;
  flex-shrink: 0;
}
.color-btn:hover {
  background: #fee2e2;
  border-color: #ef4444;
  color: #ef4444;
}

/* 圆角预设 */
.radius-presets {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}

.radius-preset-btn {
  width: 28px;
  height: 24px;
  font-size: 11px;
  color: #64748b;
  background: #f8fafc;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.15s;
  display: flex;
  align-items: center;
  justify-content: center;
}
.radius-preset-btn:hover {
  background: #f1f5f9;
  border-color: #94a3b8;
  color: #334155;
}
.radius-preset-btn.active {
  background: #dbeafe;
  border-color: #3b82f6;
  color: #2563eb;
  font-weight: 600;
}

/* 透明度值 */
.opacity-value {
  font-size: 11px;
  color: #64748b;
  min-width: 34px;
  text-align: right;
}

/* 图层按钮 */
.layer-buttons {
  display: flex;
  gap: 4px;
  flex-wrap: wrap;
}

/* 危险操作区 */
.danger-section {
  padding-top: 10px;
}

/* 箭头端点样式选择器（Excalidraw 风格图标按钮组） */
.arrow-end-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.arrow-end-btn {
  width: 32px;
  height: 24px;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  background: #f8fafc;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 4px;
  color: #334155;
  transition: all 0.15s ease;
}

.arrow-end-btn:hover {
  background: #f1f5f9;
  border-color: #94a3b8;
}

.arrow-end-btn.active {
  background: #eff6ff;
  border-color: #3b82f6;
  color: #3b82f6;
}

.arrow-end-btn svg {
  width: 28px;
  height: 16px;
}

/* 线条风格选择器 */
.line-style-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.line-style-btn {
  height: 24px;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  background: #f8fafc;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 6px;
  color: #334155;
  transition: all 0.15s ease;
}

.line-style-btn:hover {
  background: #f1f5f9;
  border-color: #94a3b8;
}

.line-style-btn.active {
  background: #eff6ff;
  border-color: #3b82f6;
  color: #3b82f6;
}

/* 文字对齐选择器 */
.align-picker {
  display: flex;
  gap: 2px;
}

.align-btn {
  width: 28px;
  height: 28px;
  border: 1px solid #e2e8f0;
  border-radius: 4px;
  background: #f8fafc;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #475569;
  transition: all 0.15s ease;
}

.align-btn:hover {
  background: #f1f5f9;
  border-color: #94a3b8;
}

.align-btn.active {
  background: #eff6ff;
  border-color: #3b82f6;
  color: #3b82f6;
}

/* ===== 导出菜单 ===== */
.export-menu {
  padding: 4px;
  min-width: 140px;
}

.export-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 10px;
  font-size: 13px;
  color: #334155;
  background: transparent;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.12s;
}

.export-menu-item:hover {
  background: #f1f5f9;
  color: #1e293b;
}

.export-menu-divider {
  height: 1px;
  margin: 4px 8px;
  background: #e2e8f0;
}

.spin {
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* ===== Mermaid Modal ===== */
.mermaid-modal-body {
  display: flex;
  gap: 16px;
  height: 480px;
}

.mermaid-modal-left {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.mermaid-modal-left-header,
.mermaid-modal-right-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 13px;
  font-weight: 500;
  color: #475569;
}

.mermaid-upload-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-left: auto;
  padding: 4px 10px;
  font-size: 12px;
  color: #64748b;
  background: #f1f5f9;
  border: 1px solid #e2e8f0;
  border-radius: 5px;
  cursor: pointer;
  transition: all 0.15s;
}

.mermaid-upload-btn:hover {
  color: #1e293b;
  background: #e2e8f0;
}

.mermaid-code-input {
  flex: 1;
  padding: 12px;
  font-family: 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
  font-size: 13px;
  line-height: 1.6;
  color: #334155;
  background: #f8fafc;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  resize: none;
  outline: none;
  transition: border-color 0.15s;
}

.mermaid-code-input:focus {
  border-color: #3b82f6;
}

.mermaid-code-input::placeholder {
  color: #94a3b8;
}

.mermaid-modal-right {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.mermaid-preview-area {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: auto;
  background: #f8fafc;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  padding: 16px;
}

.mermaid-preview-area svg {
  max-width: 100%;
  height: auto;
}

.mermaid-preview-placeholder {
  font-size: 13px;
  color: #94a3b8;
  text-align: center;
}

.mermaid-error {
  font-size: 13px;
  color: #ef4444;
  text-align: center;
  padding: 0 12px;
}

.mermaid-modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
</style>
