<!-- =====================================================================
  PropsPanel.vue — 右侧属性面板（元素样式 + 页面背景）
  ===================================================================== -->
<script setup lang="ts">
import { computed, watch, ref } from 'vue'
import { store } from '../../core/store'
import { elementTypes } from '../../core/sample'
import { generateImage, isImageConfigured, checkImageQuota } from '../../core/ai'
import { readAsDataURL } from '../../core/importer'
import { putAsset, isOssEnabled } from '../../core/assets'
import { appAlert, appConfirm, appPrompt } from '../../core/dialog'
import { markdownToSegments, segmentsToPlain, hasFormatting } from '../../core/richtext'
import AddGrid from './AddGrid.vue'
import Icon from '../common/Icon.vue'
import type { ElementType, ChartType, ShapeType } from '../../core/types'

const CHART_TYPES: Array<{ k: ChartType; label: string; icon: string }> = [
  { k: 'bar', label: '柱状图', icon: 'bar-chart' },
  { k: 'hbar', label: '条形图', icon: 'bar-chart-h' },
  { k: 'line', label: '折线图', icon: 'line-chart' },
  { k: 'area', label: '面积图', icon: 'area-chart' },
  { k: 'pie', label: '饼图', icon: 'pie' },
  { k: 'doughnut', label: '环形图', icon: 'doughnut' },
  { k: 'radar', label: '雷达图', icon: 'radar' },
  { k: 'progress', label: '进度图', icon: 'progress' }
]

const selected = computed(() => store.getSelected())
const slide = computed(() => store.currentSlide.value)
const multiCount = computed(() => store.getSelection().length)

/** 临时输入态（v-model 绑定） */
const content = ref('')
const fontSize = ref(24)
const colorSel = ref('primary')
const colorPicker = ref('#000000')
const shape = ref<ShapeType>('rect')
const chartType = ref<ChartType>('bar')
const codeLang = ref('')
const imgBusy = ref(false)
let imgAbort: AbortController | null = null

const BG_OPTIONS = [
  { k: 'bg', label: '白底' },
  { k: 'panel', label: '浅底' },
  { k: 'primary', label: '主色' },
  { k: 'accent', label: '强调' }
]

/** 选中变化时同步输入控件 */
watch(selected, (el) => {
  if (!el) return
  const s = el.style || {}
  if (el.type !== 'stat') content.value = el.content || ''
  fontSize.value = s.fontSize || 24
  shape.value = (s.shapeType as any) || 'rect'
  chartType.value = (s.chartType as ChartType) || 'bar'
  codeLang.value = s.lang || ''
  // 颜色下拉/picker 同步
  const presets = ['primary', 'accent', 'text', 'muted', '#ffffff']
  if (presets.includes(s.color || '')) {
    colorSel.value = s.color!
    colorPicker.value = '#000000'
  } else if (s.color && s.color.charAt(0) === '#') {
    colorSel.value = 'custom'
    colorPicker.value = /^#[0-9a-f]{6}$/i.test(s.color) ? s.color : '#000000'
  } else {
    colorSel.value = 'primary'
  }
}, { immediate: true })

const hasText = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula', 'shape', 'card'].includes(selected.value.type))
const canFont = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula', 'shape', 'card'].includes(selected.value.type))
const canAlign = computed(() => selected.value && ['title', 'text', 'quote', 'stat', 'table', 'formula', 'shape'].includes(selected.value.type))
const canBI = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'shape'].includes(selected.value.type))
const canColor = computed(() => selected.value && ['title', 'text', 'quote', 'list', 'stat', 'table', 'code', 'formula', 'shape'].includes(selected.value.type))
const isShape = computed(() => selected.value?.type === 'shape')

const typeLabel = computed(() => {
  const el = selected.value
  return el ? (elementTypes[el.type]?.label || el.type) : ''
})

function onAdd(type: ElementType) {
  store.addElement(type)
}

/* 多选批量操作 */
function onMultiCopy() {
  const sel = store.getSelection()
  if (sel.length) store.copyElements(sel)
}
async function onMultiDel() {
  const sel = store.getSelection()
  if (!sel.length) return
  if (await appConfirm('删除选中的 ' + sel.length + ' 个元素？', '可用 Ctrl+Z 撤销', { danger: true, okText: '删除' })) {
    store.delElements(sel)
  }
}
function onAlignOp(edge: 'left' | 'hcenter' | 'right' | 'top' | 'vmiddle' | 'bottom') {
  const sel = store.getSelection()
  if (sel.length > 1) store.alignElements(sel, edge)
}
function onDistribute(axis: 'h' | 'v') {
  const sel = store.getSelection()
  if (sel.length > 2) store.distributeElements(sel, axis)
}

function onContentInput() {
  if (!selected.value) return
  store.updateElement(selected.value.id, { content: content.value })
}
function onFontSizeInput() {
  if (!selected.value) return
  store.updateElement(selected.value.id, { style: { fontSize: Number(fontSize.value) } })
}
function onColorChange() {
  if (!selected.value) return
  if (colorSel.value === 'custom') return
  store.updateElement(selected.value.id, { style: { color: colorSel.value } })
}
function onColorPickerInput() {
  if (!selected.value) return
  store.updateElement(selected.value.id, { style: { color: colorPicker.value } })
}
function onShapeChange() {
  if (!selected.value) return
  store.updateElement(selected.value.id, { style: { shapeType: shape.value } })
}
function onChartTypeChange() {
  if (!selected.value) return
  store.updateElement(selected.value.id, { style: { chartType: chartType.value } })
}
function toggleLegend() {
  if (!selected.value) return
  const cur = selected.value.style.legend !== false
  store.updateElement(selected.value.id, { style: { legend: !cur } })
}
function toggleGrid() {
  if (!selected.value) return
  const cur = selected.value.style.grid !== false
  store.updateElement(selected.value.id, { style: { grid: !cur } })
}
function toggleHeader() {
  if (!selected.value) return
  const cur = selected.value.style.header !== false
  store.updateElement(selected.value.id, { style: { header: !cur } })
}
function onCodeLangInput() {
  if (!selected.value) return
  store.updateElement(selected.value.id, { style: { lang: codeLang.value } })
}
function onAlign(a: 'left' | 'center' | 'right') {
  if (!selected.value) return
  store.updateElement(selected.value.id, { style: { align: a } })
}
function toggleBold() {
  if (!selected.value) return
  store.updateElement(selected.value.id, { style: { bold: !selected.value.style.bold } })
}
function toggleItalic() {
  if (!selected.value) return
  store.updateElement(selected.value.id, { style: { italic: !selected.value.style.italic } })
}
function onZ(dir: number) {
  if (!selected.value) return
  store.moveElementZ(selected.value.id, dir)
}
async function onDel() {
  if (!selected.value) return
  const label = elementTypes[selected.value.type]?.label || '元素'
  if (await appConfirm('删除这个' + label + '？', '可用 Ctrl+Z 撤销', { danger: true, okText: '删除' })) {
    store.delElement(selected.value.id)
  }
}
function onBg(k: string) {
  store.setSlideBackground(k)
}

/* ---------- 富文本格式 ---------- */
/** 当前选中元素是否有 segments */
const hasRich = computed(() => {
  const el = selected.value
  return !!(el?.segments && el.segments.length && hasFormatting(el.segments))
})

/** 从 Markdown 语法生成 segments */
function applyMarkdown() {
  const el = selected.value
  if (!el) return
  const segs = markdownToSegments(content.value)
  if (segs.length && hasFormatting(segs)) {
    store.updateElement(el.id, { segments: segs })
  }
}

/** 清除 segments（降级为纯文本） */
function clearRich() {
  const el = selected.value
  if (!el) return
  store.updateElement(el.id, { segments: undefined })
}

/* ---------- AI 配图 / 本地换图 ---------- */
async function onAiImage() {
  if (imgBusy.value) return
  const el = selected.value
  if (!el || el.type !== 'image') return
  if (!isImageConfigured()) { appAlert('需要 API Key', '请先在「AI 设置」中配置'); return }
  const promptText = await appPrompt('AI 配图', { placeholder: '描述你想要的图片，如「现代办公室协作场景，俯拍，柔和光线」' })
  if (!promptText) return
  imgBusy.value = true
  imgAbort = new AbortController()
  try {
    const r = await generateImage({ prompt: promptText, signal: imgAbort.signal })
    const quotaErr = checkImageQuota(r.url)
    if (quotaErr) { appAlert('配图受限', quotaErr); return }
    store.updateElement(el.id, { content: r.url })
  } catch (e: any) {
    if (e?.name !== 'AbortError') appAlert('配图失败', e?.message || String(e))
  } finally {
    imgBusy.value = false; imgAbort = null
  }
}

/* ---------- 批注（仅图片元素） ---------- */
/** 复用文件内颜色选项集合（与颜色 select 的 option values 同款） */
const ANNO_COLORS: Array<{ k: string; label: string }> = [
  { k: 'primary', label: '主色' },
  { k: 'accent', label: '强调色' },
  { k: 'text', label: '正文色' },
  { k: 'muted', label: '次要色' },
  { k: '#ffffff', label: '白色' }
]

/** 当前图片的批注列表 */
const annotations = computed<any[]>(() => selected.value?.style?.annotations || [])

/** 折叠状态：记录「已展开」的批注 id（默认折叠，仅展开正在编辑的一条） */
const expandedAnno = ref<string | null>(null)
function toggleAnno(annoId: string) {
  expandedAnno.value = expandedAnno.value === annoId ? null : annoId
}

function addAnno() {
  if (!selected.value) return
  const id = store.addAnnotation(selected.value.id)
  if (id) expandedAnno.value = id   // 新增自动展开
}
function delAnno(annoId: string) {
  if (selected.value) store.delAnnotation(selected.value.id, annoId)
  if (expandedAnno.value === annoId) expandedAnno.value = null
}
function patchAnno(annoId: string, patch: Record<string, any>) {
  if (!selected.value) return
  store.updateAnnotation(selected.value.id, annoId, patch)
}

/** 批注文字摘要（折叠态卡片头显示） */
function annoSummary(anno: any): string {
  const t = (anno.text || '').trim()
  return t ? (t.length > 12 ? t.slice(0, 12) + '…' : t) : '（空批注）'
}

/** 本地上传图片替换当前图片元素内容 */
function onLocalImage() {
  const el = selected.value
  if (!el || el.type !== 'image') return
  const input = document.createElement('input')
  input.type = 'file'
  input.accept = 'image/*'
  input.onchange = async () => {
    const file = input.files?.[0]
    if (!file) return
    try {
      // OSS 启用：走资产库（上云或离线暂存），content 存引用/URL，绕过 5MB 墙
      if (isOssEnabled()) {
        const ref = await putAsset(file)
        store.updateElement(el.id, { content: ref })
        return
      }
      const dataUrl = await readAsDataURL(file)
      const quotaErr = checkImageQuota(dataUrl)
      if (quotaErr) { appAlert('图片受限', quotaErr); return }
      store.updateElement(el.id, { content: dataUrl })
    } catch (e: any) {
      appAlert('读取图片失败', e?.message || String(e))
    }
  }
  input.click()
}

/* ---------- 视频元素 ---------- */
function onLocalVideo() {
  const el = selected.value
  if (!el) return
  const input = document.createElement('input')
  input.type = 'file'
  input.accept = 'video/*'
  input.onchange = async () => {
    const f = input.files?.[0]
    if (!f) return
    // OSS 启用：视频走资产库（大文件尤其受益，避免撑爆本地存储）
    if (isOssEnabled()) {
      try {
        const ref = await putAsset(f)
        store.updateElement(el.id, { content: ref })
      } catch (e: any) {
        appAlert('读取视频失败', e?.message || String(e))
      }
      return
    }
    // 注意：大视频会整体读入内存（dataURL），本地工具首版接受
    const dataUrl = await readAsDataURL(f)
    store.updateElement(el.id, { content: dataUrl })
  }
  input.click()
}
function onVideoUrl(e: Event) {
  const el = selected.value
  if (!el) return
  const v = (e.target as HTMLInputElement).value.trim()
  if (v) store.updateElement(el.id, { content: v })
}
function toggleVideoOpt(key: 'autoplay' | 'loop' | 'muted') {
  const el = selected.value
  if (!el) return
  const cur = (el.style as any)[key]
  store.updateElement(el.id, { style: { [key]: !cur } })
}
</script>

<template>
  <div class="panel-pane props-pane" id="panelProps">
    <!-- 空态 -->
    <section v-if="!selected" class="prop-section" id="propEmpty">
      <p class="prop-hint">点击画布元素以编辑样式<br />或在下方添加新元素</p>
      <AddGrid @add="onAdd" />
    </section>

    <!-- 多选态：批量操作 -->
    <section v-else-if="multiCount > 1" class="prop-section" id="propMulti">
      <h4 class="prop-title">多选 · <span>{{ multiCount }} 个元素</span></h4>
      <div class="prop-row">
        <label>批量操作</label>
        <div class="seg">
          <button @click="onMultiCopy" title="Ctrl+C">⧉ 复制</button>
          <button class="danger" @click="onMultiDel" title="Delete"><Icon name="trash" :size="13" /> 删除</button>
        </div>
      </div>
      <div class="prop-row">
        <label>对齐</label>
        <div class="seg">
          <button @click="onAlignOp('left')" title="左对齐">⇤</button>
          <button @click="onAlignOp('hcenter')" title="水平居中">↔</button>
          <button @click="onAlignOp('right')" title="右对齐">⇥</button>
          <button @click="onAlignOp('top')" title="顶对齐">⇧</button>
          <button @click="onAlignOp('vmiddle')" title="垂直居中">↕</button>
          <button @click="onAlignOp('bottom')" title="底对齐">⇩</button>
        </div>
      </div>
      <div class="prop-row">
        <label>分布</label>
        <div class="seg">
          <button :disabled="multiCount < 3" @click="onDistribute('h')" :title="multiCount < 3 ? '需要选中 3 个以上' : '水平等间距'">⋯</button>
          <button :disabled="multiCount < 3" @click="onDistribute('v')" :title="multiCount < 3 ? '需要选中 3 个以上' : '垂直等间距'">⋮</button>
        </div>
      </div>
      <div class="prop-row">
        <label>提示</label>
        <div style="font-size:12px;color:var(--ui-muted);line-height:1.6">
          拖动整组移动 · Shift+点击加选/减选<br />空白处拖动框选 · Esc 取消多选
        </div>
      </div>
    </section>

    <!-- 选中元素 -->
    <section v-else class="prop-section" id="propElement">
      <h4 class="prop-title">元素 · <span>{{ typeLabel }}</span></h4>

      <!-- 内容 -->
      <div v-if="hasText && selected.type !== 'stat'" class="prop-row">
        <label>内容</label>
        <textarea
          rows="3"
          :placeholder="selected.type === 'card' ? '第一行=标题，其余行=正文' : selected.type === 'shape' ? '形状内文字（支持 **加粗** 等 md 语法）' : '输入文字（列表用换行分隔）'"
          v-model="content"
          @input="onContentInput"
        ></textarea>
        <!-- 富文本格式提示 -->
        <div v-if="['title','text','quote','list'].includes(selected.type)" class="rich-hint">
          <div class="rich-syntax">
            **加粗** · *斜体* · ==高亮== · ~~删除线~~ · `代码` · ^上标^ · ~下标~
          </div>
          <div class="rich-actions">
            <button v-if="hasRich" class="rich-btn danger" @click="clearRich">清除格式</button>
            <button class="rich-btn" @click="applyMarkdown" title="把上面的 Markdown 语法转为富文本">转换格式</button>
          </div>
        </div>
      </div>

      <!-- 字号 -->
      <div v-if="canFont" class="prop-row">
        <label>字号 <span>{{ fontSize }}px</span></label>
        <input type="range" min="12" max="120" v-model.number="fontSize" @input="onFontSizeInput" />
      </div>

      <!-- 颜色 -->
      <div v-if="canColor" class="prop-row">
        <label>颜色</label>
        <div class="color-row">
          <select v-model="colorSel" @change="onColorChange">
            <option value="primary">主色</option>
            <option value="accent">强调色</option>
            <option value="text">正文色</option>
            <option value="muted">次要色</option>
            <option value="#ffffff">白色</option>
            <option value="custom">自定义…</option>
          </select>
          <input type="color" v-model="colorPicker" v-show="colorSel === 'custom'" @input="onColorPickerInput" />
        </div>
      </div>

      <!-- 对齐 -->
      <div v-if="canAlign" class="prop-row">
        <label>对齐</label>
        <div class="seg">
          <button :class="{ active: selected.style.align === 'left' }" @click="onAlign('left')" title="左对齐"><Icon name="align-left" :size="14" /></button>
          <button :class="{ active: selected.style.align === 'center' || !selected.style.align }" @click="onAlign('center')" title="居中"><Icon name="align-center" :size="14" /></button>
          <button :class="{ active: selected.style.align === 'right' }" @click="onAlign('right')" title="右对齐"><Icon name="align-right" :size="14" /></button>
        </div>
      </div>

      <!-- 加粗/斜体 -->
      <div v-if="canBI" class="prop-row">
        <label>样式</label>
        <div class="seg">
          <button :class="{ active: !!selected.style.bold }" @click="toggleBold" title="加粗"><b>B</b></button>
          <button :class="{ active: !!selected.style.italic }" @click="toggleItalic" title="斜体"><i>I</i></button>
        </div>
      </div>

      <!-- 形状 -->
      <div v-if="isShape" class="prop-row">
        <label>形状</label>
        <select v-model="shape" @change="onShapeChange">
          <option value="rect">矩形</option>
          <option value="circle">圆形</option>
          <option value="ellipse">椭圆</option>
          <option value="triangle">三角</option>
          <option value="diamond">菱形</option>
          <option value="pentagon">五边形</option>
          <option value="hexagon">六边形</option>
          <option value="star">五角星</option>
          <option value="arrow">箭头</option>
          <option value="chevron">折角</option>
          <option value="bubble">气泡</option>
        </select>
      </div>

      <!-- 图表配置 -->
      <template v-if="selected.type === 'chart'">
        <div class="prop-row">
          <label>图表类型</label>
          <select v-model="chartType" @change="onChartTypeChange">
            <option v-for="t in CHART_TYPES" :key="t.k" :value="t.k">{{ t.label }}</option>
          </select>
        </div>
        <div class="prop-row">
          <label>显示选项</label>
          <div class="seg">
            <button :class="{ active: selected.style.legend !== false }" @click="toggleLegend" title="图例">图例</button>
            <button :class="{ active: selected.style.grid !== false }" @click="toggleGrid" title="网格线">网格</button>
          </div>
        </div>
        <div class="prop-row">
          <label>数据格式</label>
          <div style="font-size:11px;color:var(--ui-muted,#64748b);line-height:1.6;padding:.3em 0">
            单系列：[{"label":"A","value":65}]<br />
            多系列：{"series":["Q1","Q2"],"items":[{"label":"华东","values":[120,150]}]}
          </div>
        </div>
      </template>

      <!-- 表格选项 -->
      <div v-if="selected.type === 'table'" class="prop-row">
        <label>首行表头</label>
        <div class="seg">
          <button :class="{ active: selected.style.header !== false }" @click="toggleHeader">表头</button>
        </div>
      </div>

      <!-- 代码语言 -->
      <div v-if="selected.type === 'code'" class="prop-row">
        <label>语言</label>
        <input type="text" v-model="codeLang" @input="onCodeLangInput" placeholder="js / python / ..." />
      </div>

      <!-- 公式提示 -->
      <div v-if="selected.type === 'formula'" class="prop-row">
        <label>提示</label>
        <div style="font-size:12px;color:var(--ui-muted);line-height:1.6">
          LaTeX 语法。示例：<br />
          E = mc^2<br />
          \frac{a}{b}<br />
          \sum_{i=1}^n x_i
        </div>
      </div>

      <!-- 图片来源（仅图片元素） -->
      <div v-if="selected.type === 'image'" class="prop-row">
        <label>图片来源</label>
        <div class="seg">
          <button @click="onLocalImage" title="从本地选择图片"><Icon name="folder" :size="13" /> 本地图片</button>
          <button :disabled="imgBusy" @click="onAiImage"><template v-if="imgBusy">生成中…</template><template v-else><Icon name="palette" :size="13" /> AI 配图</template></button>
        </div>
      </div>

      <!-- 视频来源（仅视频元素） -->
      <div v-if="selected.type === 'video'" class="prop-row">
        <label>视频来源</label>
        <div class="seg">
          <button @click="onLocalVideo" title="从本地选择视频文件"><Icon name="folder" :size="13" /> 本地视频</button>
        </div>
        <input
          type="text"
          class="video-url"
          placeholder="或粘贴视频 URL (https://…)"
          style="margin-top:6px;width:100%;box-sizing:border-box"
          :value="selected.content"
          @change="onVideoUrl($event)"
        />
      </div>
      <!-- 视频播放选项 -->
      <div v-if="selected.type === 'video'" class="prop-row">
        <label>播放选项</label>
        <div class="seg">
          <button :class="{ active: !!selected.style.autoplay }" @click="toggleVideoOpt('autoplay')" title="自动播放">自动</button>
          <button :class="{ active: !!selected.style.loop }" @click="toggleVideoOpt('loop')" title="循环">循环</button>
          <button :class="{ active: !!selected.style.muted }" @click="toggleVideoOpt('muted')" title="静音">静音</button>
        </div>
      </div>

      <!-- 层级 -->
      <div class="prop-row">
        <label>层级</label>
        <div class="seg">
          <button @click="onZ(1)" title="上移">↑</button>
          <button @click="onZ(-1)" title="下移">↓</button>
          <button class="danger" @click="onDel" title="删除"><Icon name="trash" :size="13" /></button>
        </div>
      </div>

      <!-- 批注（仅图片元素） -->
      <div v-if="selected.type === 'image'" class="prop-row">
        <label>批注</label>
        <div class="anno-head">
          <button @click="addAnno" title="添加一条批注">＋ 添加批注</button>
        </div>
        <div class="anno-list">
          <div v-for="(anno, i) in annotations" :key="anno.id" class="anno-card" :class="{ open: expandedAnno === anno.id }">
            <div class="anno-card-head" @click="toggleAnno(anno.id)">
              <span class="anno-caret">{{ expandedAnno === anno.id ? '▾' : '▸' }}</span>
              <span class="anno-card-title">批注 {{ i + 1 }}</span>
              <span v-if="expandedAnno !== anno.id" class="anno-card-summary">{{ annoSummary(anno) }}</span>
              <button class="anno-del" @click.stop="delAnno(anno.id)" title="删除批注">×</button>
            </div>

            <div v-show="expandedAnno === anno.id" class="anno-body">
              <textarea
                class="anno-text"
                rows="2"
                placeholder="批注文字"
                :value="anno.text"
                @input="patchAnno(anno.id, { text: ($event.target as HTMLTextAreaElement).value })"
              ></textarea>

              <!-- 气泡文字样式 -->
              <div class="anno-sub">气泡文字</div>
              <div class="anno-line">
                <label class="anno-lbl">字号 {{ anno.fontSize || 16 }}px</label>
                <input
                  type="range" min="12" max="48"
                  :value="anno.fontSize || 16"
                  @input="patchAnno(anno.id, { fontSize: Number(($event.target as HTMLInputElement).value) })"
                />
              </div>
              <div class="anno-line">
                <select
                  :value="anno.color || 'text'"
                  @change="patchAnno(anno.id, { color: ($event.target as HTMLSelectElement).value })"
                >
                  <option v-for="c in ANNO_COLORS" :key="c.k" :value="c.k">{{ c.label }}</option>
                </select>
                <div class="seg">
                  <button :class="{ active: !!anno.bold }" @click="patchAnno(anno.id, { bold: !anno.bold })" title="加粗"><b>B</b></button>
                  <button :class="{ active: !!anno.italic }" @click="patchAnno(anno.id, { italic: !anno.italic })" title="斜体"><i>I</i></button>
                </div>
              </div>
              <div class="anno-line">
                <div class="seg">
                  <button :class="{ active: anno.align === 'left' }" @click="patchAnno(anno.id, { align: 'left' })" title="左对齐"><Icon name="align-left" :size="14" /></button>
                  <button :class="{ active: anno.align === 'center' || !anno.align }" @click="patchAnno(anno.id, { align: 'center' })" title="居中"><Icon name="align-center" :size="14" /></button>
                  <button :class="{ active: anno.align === 'right' }" @click="patchAnno(anno.id, { align: 'right' })" title="右对齐"><Icon name="align-right" :size="14" /></button>
                </div>
              </div>

              <!-- 连线样式 -->
              <div class="anno-sub">连线</div>
              <div class="anno-line">
                <select
                  :value="anno.line?.style || 'solid'"
                  @change="patchAnno(anno.id, { line: { style: ($event.target as HTMLSelectElement).value } })"
                >
                  <option value="solid">实线</option>
                  <option value="dashed">虚线</option>
                </select>
                <select
                  :value="anno.line?.color || 'muted'"
                  @change="patchAnno(anno.id, { line: { color: ($event.target as HTMLSelectElement).value } })"
                >
                  <option v-for="c in ANNO_COLORS" :key="c.k" :value="c.k">{{ c.label }}</option>
                </select>
              </div>
              <div class="anno-line">
                <label class="anno-lbl">线宽 {{ anno.line?.width ?? 0.5 }}px</label>
                <input
                  type="range" min="0.5" max="8" step="0.5"
                  :value="anno.line?.width ?? 0.5"
                  @input="patchAnno(anno.id, { line: { width: Number(($event.target as HTMLInputElement).value) } })"
                />
              </div>
              <div class="anno-line">
                <select
                  :value="anno.line?.startCap || 'none'"
                  @change="patchAnno(anno.id, { line: { startCap: ($event.target as HTMLSelectElement).value } })"
                >
                  <option value="none">起点·无</option>
                  <option value="arrow">起点·箭头</option>
                  <option value="dot">起点·圆点</option>
                </select>
                <select
                  :value="anno.line?.endCap || 'arrow'"
                  @change="patchAnno(anno.id, { line: { endCap: ($event.target as HTMLSelectElement).value } })"
                >
                  <option value="none">终点·无</option>
                  <option value="arrow">终点·箭头</option>
                  <option value="dot">终点·圆点</option>
                </select>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>

    <!-- 当前页背景 -->
    <section class="prop-section">
      <h4 class="prop-title">当前页背景</h4>
      <div class="bg-grid">
        <button
          v-for="b in BG_OPTIONS"
          :key="b.k"
          :class="{ active: slide.background === b.k }"
          @click="onBg(b.k)"
        >{{ b.label }}</button>
      </div>
    </section>
  </div>
</template>

<style scoped>
.anno-head {
  display: flex;
  margin-bottom: 6px;
}
.anno-head button {
  flex: 1;
  height: 32px;
  border: 1px solid var(--ui-border, #e2e8f0);
  border-radius: var(--radius-sm, 6px);
  background: #fff;
  color: var(--ui-text, #1e293b);
  font-size: 13px;
  cursor: pointer;
  transition: background .15s, border-color .15s;
}
.anno-head button:hover {
  background: var(--ui-primary-soft, #eeeefc);
  border-color: var(--ui-primary, #5b5bd6);
  color: var(--ui-primary, #5b5bd6);
}
.anno-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.anno-card {
  border: 0.5px solid var(--ui-border, #e2e8f0);
  border-radius: 8px;
  padding: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--ui-panel, #f8fafc);
}
.anno-card.open {
  border-color: var(--ui-primary, #5b5bd6);
}
.anno-card-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 7px 8px;
  cursor: pointer;
  user-select: none;
}
.anno-card-head:hover {
  background: var(--ui-primary-soft, #eeeefc);
}
.anno-caret {
  font-size: 10px;
  color: var(--ui-muted, #64748b);
  width: 12px;
  flex: none;
}
.anno-card-title {
  font-size: 12px;
  font-weight: 500;
  color: var(--ui-text, #1e293b);
  flex: none;
}
.anno-card-summary {
  font-size: 12px;
  color: var(--ui-muted, #64748b);
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.anno-del {
  margin-left: auto;
  width: 20px;
  height: 20px;
  line-height: 1;
  padding: 0;
  border-radius: 6px;
  border: 1px solid var(--ui-border, #e2e8f0);
  background: #fff;
  color: var(--ui-muted, #64748b);
  cursor: pointer;
  flex: none;
}
.anno-del:hover {
  background: #fff1f2;
  border-color: var(--ui-danger, #ef4444);
  color: var(--ui-danger, #ef4444);
}
.anno-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0 8px 8px;
}
.anno-text {
  width: 100%;
  resize: vertical;
}
.anno-sub {
  font-size: 11px;
  color: var(--ui-muted, #64748b);
  margin-top: 2px;
}
.anno-line {
  display: flex;
  align-items: center;
  gap: 6px;
}
.anno-line > select {
  flex: 1;
  min-width: 0;
}
.anno-lbl {
  font-size: 12px;
  color: var(--ui-muted, #64748b);
  white-space: nowrap;
}
.anno-line > input[type="range"] {
  flex: 1;
}
</style>

