<template>
  <div class="editor-page">
    <div class="editor-body">
      <!-- 左侧面板 -->
      <div class="left-panel" :style="{ width: leftPanelWidth + 'px' }">
        <!-- 顶部 Logo -->
        <div class="sidebar-header">
          <div class="sidebar-logo">
            <span class="logo-icon">✦</span>
            <span class="logo-text">灵越智报</span>
          </div>
          <div class="sidebar-header-actions">
            <el-button text circle size="small" title="通知"><el-icon><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg></el-icon></el-button>
          </div>
        </div>

        <!-- 快捷操作 -->
        <div class="sidebar-nav">
          <div class="nav-item" @click="showNewProjectDialog = true">
            <span class="nav-icon">📝</span>
            <span class="nav-label">新建报告</span>
          </div>
          <div class="nav-item" @click="showAttachmentDialog = true">
            <span class="nav-icon">📚</span>
            <span class="nav-label">知识库</span>
          </div>
        </div>

        <!-- 我的文档 -->
        <div class="sidebar-section">
          <div class="section-header">
            <span class="section-title">我的文档 · {{ projects.length }}</span>
            <span class="section-action" @click="leftPanelTab = 'projects'">全部 ›</span>
          </div>

          <!-- 搜索框 -->
          <el-input
            v-if="projects.length > 5"
            v-model="projectSearchKeyword"
            placeholder="搜索文档..."
            :prefix-icon="Search"
            clearable
            class="sidebar-search"
            size="small"
          />

          <div class="doc-list" v-if="filteredProjects.length > 0">
            <div
              v-for="project in filteredProjects.slice(0, sidebarShowAll ? undefined : 5)"
              :key="project.id"
              class="doc-item"
              :class="{ active: currentProjectId === project.id }"
              @click="switchProject(project)"
            >
              <div class="doc-icon-wrap">
                <span class="doc-icon-glyph">📋</span>
              </div>
              <div class="doc-item-body">
                <div class="doc-item-title">{{ project.title }}</div>
                <!-- 解析进度条 -->
                <div v-if="parsingProjectId === project.id" class="doc-item-progress">
                  <el-progress 
                    :percentage="docxParseProgress" 
                    :status="docxParseStatus || undefined"
                    :stroke-width="4"
                    :show-text="false"
                  />
                  <span class="progress-text">{{ docxParseMessage }}</span>
                </div>
                <div v-else class="doc-item-meta">
                  <el-tag
                    size="small"
                    :type="project.status === 'archived' ? 'success' : project.status === 'editing' ? '' : 'warning'"
                    effect="plain"
                    class="doc-status-tag"
                  >{{ getStatusText(project.status) }}</el-tag>
                  <span class="doc-item-date">{{ formatTime(project.updatedAt || project.createdAt) }}</span>
                  <span class="doc-item-author">· {{ project.createdBy || userName }}</span>
                </div>
              </div>
              <el-dropdown trigger="click" @command="(cmd) => handleProjectCommand(cmd, project)" @click.stop>
                <el-button class="doc-more-btn" text size="small" @click.stop>
                  <el-icon><MoreFilled /></el-icon>
                </el-button>
                <template #dropdown>
                  <el-dropdown-menu>
                    <el-dropdown-item command="copy">复制项目</el-dropdown-item>
                    <el-dropdown-item command="archive">归档</el-dropdown-item>
                    <el-dropdown-item command="delete" divided>删除</el-dropdown-item>
                  </el-dropdown-menu>
                </template>
              </el-dropdown>
            </div>
          </div>

          <div class="doc-empty" v-else-if="!loadingProjects">
            <span class="empty-text">{{ projectSearchKeyword ? '未找到匹配的文档' : '暂无文档' }}</span>
          </div>

          <div class="doc-loading" v-if="loadingProjects">
            <el-icon class="is-loading"><Loading /></el-icon>
            <span>加载中...</span>
          </div>
        </div>

        <!-- 最近操作 -->
        <div class="sidebar-section sidebar-activity">
          <div class="section-header">
            <span class="section-title">最近操作</span>
            <span class="section-action" title="筛选">▽</span>
          </div>
          <div class="activity-list">
            <div class="activity-item" v-for="(act, idx) in recentActivities" :key="idx">
              <div class="activity-text">{{ act.text }}</div>
              <div class="activity-meta">
                <span class="activity-source">{{ act.source }}</span>
                <span class="activity-time">{{ act.time }}</span>
              </div>
            </div>
            <div class="activity-empty" v-if="recentActivities.length === 0">
              <span>暂无最近操作</span>
            </div>
          </div>
        </div>

        <!-- 底部用户信息 -->
        <div class="sidebar-footer">
          <div class="user-avatar">{{ userName.charAt(0) }}</div>
          <div class="user-info">
            <span class="user-name">{{ userName }}</span>
            <span class="user-role">项目经理</span>
          </div>
          <div class="footer-actions">
            <el-badge :value="taskRunningCount" :hidden="taskRunningCount === 0" :max="99" class="notification-badge">
              <el-button text circle size="small" title="任务中心" @click="taskCenterStore.toggleOpen()"><el-icon><List /></el-icon></el-button>
            </el-badge>
            <el-badge :value="3" :max="99" class="notification-badge">
              <el-button text circle size="small" title="消息"><el-icon><svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></el-icon></el-button>
            </el-badge>
            <el-dropdown trigger="click" @command="handleFooterCommand">
              <el-button text circle size="small" title="设置"><el-icon><MoreFilled /></el-icon></el-button>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item command="settings">系统设置</el-dropdown-item>
                  <el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
          </div>
        </div>


      </div>

      <!-- 左侧拖拽分隔条 -->
      <div class="resize-handle left-resize" @mousedown="startResizeLeft"></div>

      <!-- 中间主区域 -->
      <div class="center-panel">
        <!-- 欢迎页 -->
        <div class="welcome-page" v-if="!hasActiveProject">
          <div class="welcome-content">
            <div class="welcome-logo">灵</div>
            <div class="welcome">
              <h1>{{ greetingText }}，{{ userName }}！<span>智能报告，洞察未来。</span></h1>
              <p>今天是个创作的好日子，开始您的智能报告之旅吧</p>
              <p class="welcome-version">v0.2.2</p>
            </div>
          </div>
        </div>

        <!-- 项目详情 -->
        <template v-else>
          <div class="editor-title-bar">
            <div class="titlebar-left">
              <el-icon class="titlebar-folder-icon"><Folder /></el-icon>
              <span class="titlebar-sep">/</span>
              <span class="titlebar-project-name" :title="projectTitle">{{ projectTitle || '未命名项目' }}</span>
              <el-tag size="small" type="info" class="titlebar-status-tag">草稿</el-tag>
            </div>
            <div class="titlebar-right">
              <span class="titlebar-save-status">
                <span class="save-dot" :class="{ saved: saved }"></span>
                {{ saved ? '已自动保存' : '保存中...' }}
              </span>
              <el-divider direction="vertical" />
              <el-button text circle size="small" :class="{ 'is-active-view': viewMode === 'document' }" title="文档视图" @click="viewMode = 'document'"><el-icon><Document /></el-icon></el-button>
              <el-button text circle size="small" :class="{ 'is-active-view': viewMode === 'elements' }" title="要素视图" @click="viewMode = 'elements'"><el-icon><Grid /></el-icon></el-button>
              <el-button text circle size="small" title="设置" @click="ElMessage.info('设置开发中...')"><el-icon><Setting /></el-icon></el-button>
              <el-dropdown trigger="click" @command="handleTitlebarCommand">
                <el-button text circle size="small" title="更多"><el-icon><MoreFilled /></el-icon></el-button>
                <template #dropdown>
                  <el-dropdown-menu>
                    <el-dropdown-item command="save">💾 保存</el-dropdown-item>
                    <el-dropdown-item command="copy">📋 复制项目</el-dropdown-item>
                    <el-dropdown-item command="export" divided>📤 导出</el-dropdown-item>
                  </el-dropdown-menu>
                </template>
              </el-dropdown>
              <el-divider direction="vertical" />
              <el-button text circle size="small" title="附件" @click="showAttachmentDialog = true"><el-icon><Paperclip /></el-icon></el-button>
              <el-button text circle size="small" title="规则" @click="showRuleDialog = true"><el-icon><List /></el-icon></el-button>
            </div>
          </div>

          <div class="editor-scroll" ref="editorRef" v-loading="loading" element-loading-text="正在加载项目...">
            <!-- 文档视图 -->
            <div class="document-view" v-if="viewMode === 'document'">
              <!-- 文档渲染区域（可编辑） -->
              <div
                class="doc-paper"
                v-if="docHtml"
                :style="docPaperStyle"
                contenteditable="true"
                spellcheck="false"
                v-html="docHtml"
                @input="onDocInput"
                @mousedown="onDocClick"
                ref="docPaperRef"
              ></div>

              <!-- 无内容提示 -->
              <div class="doc-empty" v-else-if="!docLoading">
                <el-empty description="暂无文档内容" />
              </div>
              <div class="doc-loading" v-if="docLoading">
                <el-icon class="is-loading" :size="24"><Loading /></el-icon>
                <span>正在加载文档内容...</span>
              </div>
            </div>

            <!-- 要素高亮弹出框 -->
            <div
              v-if="highlightPopover.visible"
              class="element-popover"
              :style="{ top: highlightPopover.y + 'px', left: highlightPopover.x + 'px' }"
              @mousedown.stop
            >
              <div class="popover-header">
                <span class="popover-label">{{ highlightPopover.elementName }}</span>
                <el-tag size="small">{{ highlightPopover.elementKey }}</el-tag>
              </div>
              <div class="popover-body">
                <div class="popover-field">
                  <span class="popover-field-label">当前值：</span>
                  <el-input
                    v-model="highlightPopover.currentValue"
                    size="small"
                    placeholder="输入要素值"
                    @keyup.enter="savePopoverValue"
                  />
                </div>
                <div class="popover-field" v-if="highlightPopover.originalValue">
                  <span class="popover-field-label">原始值：</span>
                  <span class="popover-original">{{ highlightPopover.originalValue }}</span>
                </div>
                <!-- 溯源卡片 -->
                <div class="popover-rules" v-if="popoverRelatedRules.length > 0">
                  <span class="popover-field-label">来源规则：</span>
                  <div
                    v-for="rule in popoverRelatedRules"
                    :key="rule.id"
                    class="rule-trace-card clickable"
                    @click="openRuleWorkflow(rule)"
                    title="点击编辑规则"
                  >
                    <span class="rule-trace-action" :class="'action-' + rule.actionType">{{ ruleActionLabel(rule.actionType) }}</span>
                    <div class="rule-trace-info">
                      <div class="rule-trace-name">{{ rule.ruleName }}</div>
                      <div v-if="rule.inputs && rule.inputs.length" class="rule-trace-sources">
                        <span 
                          v-for="inp in rule.inputs" 
                          :key="inp.inputId" 
                          class="rule-trace-att clickable"
                          @click.stop="openSourceInViewer(inp, rule)"
                          title="点击查看来源"
                        >📎 {{ formatInputSource(inp) }}</span>
                      </div>
                      <!-- 显示来源摘要 -->
                      <div v-if="getInputSourceText(rule)" class="rule-trace-source-text">
                        <span class="source-text-label">来源段落：</span>
                        <span class="source-text-content">{{ getInputSourceText(rule) }}</span>
                      </div>
                      <div v-else-if="ruleSourceText(rule)" class="rule-trace-excerpt">
                        <span class="rule-trace-excerpt-text">{{ ruleSourceText(rule) }}</span>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
              <div class="popover-footer">
                <el-button size="small" @click="highlightPopover.visible = false">关闭</el-button>
                <el-button size="small" type="primary" @click="savePopoverValue">保存</el-button>
              </div>
            </div>

            <!-- 要素视图：模板文档 + {{key}} 占位符 -->
            <div class="document-view" v-if="viewMode === 'elements'">
              <div
                class="doc-paper"
                v-if="docTemplateHtml"
                :style="docPaperStyle"
                contenteditable="false"
                spellcheck="false"
                v-html="docTemplateHtml"
              ></div>

              <div class="doc-empty" v-else-if="!docLoading">
                <el-empty description="暂无文档内容" />
              </div>
            </div>

          </div>
        </template>
      </div>

      <!-- 右侧拖拽分隔条 -->
      <div v-if="hasActiveProject" class="resize-handle right-resize" @mousedown="startResizeRight"></div>

      <!-- 右侧面板 -->
      <div v-if="hasActiveProject" class="right-panel" :style="{ width: rightPanelWidth + 'px' }">
        <!-- 报告要素区 -->
        <div class="rp-elements">
          <div class="rp-elements-header">
            <div class="rp-elements-title">
              <span class="rp-title-icon">📋</span>
              <span class="rp-title-text">报告要素</span>
              <span class="rp-title-count">{{ filledValues.length }}</span>
            </div>
            <div class="rp-header-actions">
              <el-input
                v-if="elementSearchVisible"
                v-model="elementSearchQuery"
                size="small"
                placeholder="搜索要素..."
                clearable
                style="width: 120px"
                @blur="elementSearchVisible = elementSearchQuery.length > 0"
              />
              <el-button v-else text circle size="small" title="搜索" @click="elementSearchVisible = true"><el-icon><Search /></el-icon></el-button>
            </div>
          </div>
          <div class="rp-elements-body">
            <div class="rp-element-list" v-if="groupedElements.length > 0">
              <div 
                v-for="group in groupedElements" 
                :key="group.namespace" 
                class="rp-element-group"
              >
                <div class="rp-group-header" @click="toggleElementGroup(group.namespace)">
                  <span class="rp-group-icon">{{ elementGroupExpanded[group.namespace] ? '▼' : '▶' }}</span>
                  <span class="rp-group-name">{{ group.label }}</span>
                  <span class="rp-group-count">{{ group.items.length }}</span>
                </div>
                <div class="rp-group-items" v-show="elementGroupExpanded[group.namespace]">
                  <div 
                    v-for="item in group.items" 
                    :key="item.elementKey"
                    class="rp-element-item"
                    :class="{ 
                      'is-text': item.elementType === 'text',
                      'is-paragraph': item.elementType === 'paragraph',
                      'is-table': item.elementType === 'table',
                      'has-value': item.hasValue,
                      'is-active': highlightPopover.elementKey === item.elementKey
                    }"
                    @click="scrollToElement(item)"
                    :title="item.valueText || '暂无值'"
                  >
                    <span class="rp-item-type">{{ item.elementType === 'text' ? 'T' : item.elementType === 'paragraph' ? 'P' : '▦' }}</span>
                    <span class="rp-item-name">{{ item.elementName }}</span>
                    <span v-if="item.hasValue" class="rp-item-preview">{{ truncateValue(item.valueText, 20) }}</span>
                  </div>
                </div>
              </div>
            </div>
            <div class="rp-elements-empty" v-else>
              <span>暂无要素</span>
            </div>
          </div>
        </div>

        <!-- AI 助手区 -->
        <div class="rp-ai">
          <div class="rp-ai-header">
            <div class="rp-ai-title">
              <span class="rp-ai-icon">🤖</span>
              <span>AI 助手</span>
            </div>
            <div class="rp-ai-actions">
              <el-button text size="small" @click="aiMessages = []">🔄 新对话</el-button>
            </div>
          </div>
          <div class="rp-ai-messages" ref="aiMessagesRef">
            <div class="ai-message ai-bot" v-if="aiMessages.length === 0">
              <div class="ai-bubble">👋 您好，我不仅是您的AI助手，更是您深度思考时的沉浸式创作搭档。你准备好开启这场创作之旅了吗？</div>
            </div>
            <div
              v-for="(msg, idx) in aiMessages"
              :key="idx"
              class="ai-message"
              :class="msg.role === 'user' ? 'ai-user' : 'ai-bot'"
            >
              <div class="ai-bubble">{{ msg.content }}</div>
            </div>
          </div>
          <div class="rp-ai-input">
            <el-input
              v-model="aiInputText"
              placeholder="发消息给 AI 助手～"
              :autosize="{ minRows: 1, maxRows: 3 }"
              type="textarea"
              resize="none"
              @keydown.enter.exact.prevent="sendAiMessage"
            />
            <div class="rp-ai-input-actions">
              <div class="rp-ai-input-tools">
                <el-button text circle size="small" title="附加">＋</el-button>
                <el-button text circle size="small" title="提及">@</el-button>
                <el-button text circle size="small" title="模板">🌐</el-button>
              </div>
              <div class="rp-ai-input-right">
                <el-button text circle size="small" title="语音">🎤</el-button>
                <el-button
                  type="primary"
                  circle
                  size="small"
                  :disabled="!aiInputText.trim()"
                  @click="sendAiMessage"
                  title="发送"
                >
                  <el-icon><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg></el-icon>
                </el-button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 附件管理弹窗 -->
    <el-dialog
      v-model="showAttachmentDialog"
      title="📎 附件管理"
      width="640"
      :close-on-click-modal="true"
      class="floating-panel-dialog"
      align-center
    >
      <div class="fp-toolbar">
        <el-upload
          :auto-upload="false"
          :on-change="handleAttachmentUpload"
          :show-file-list="false"
          accept=".pdf,.doc,.docx,.xls,.xlsx,.zip,.png,.jpg"
        >
          <el-button size="small" :icon="Plus">添加附件</el-button>
        </el-upload>
        <span class="fp-count">共 {{ attachments.length }} 个附件</span>
      </div>
      <div class="fp-list">
        <div
          v-for="att in attachments"
          :key="att.id"
          class="fp-att-item"
          :class="{ active: selectedAttachment?.id === att.id }"
          @click="selectAttachment(att)"
        >
          <div class="att-icon" :class="getFileTypeClass(att)">
            <span class="att-icon-text">{{ getFileTypeLabel(att) }}</span>
          </div>
          <div class="att-info">
            <div class="att-name" :title="att.displayName">{{ att.displayName }}</div>
            <div class="att-meta">
              <span class="att-type">{{ getFileTypeTag(att) }}</span>
              <span class="att-size">{{ formatFileSize(att.fileSize) }}</span>
              <span
                v-if="parseStates[att.id]?.status === 'parsing' || parseStates[att.id]?.status === 'uploading'"
                class="att-parse-status parsing"
              >
                <el-icon class="is-loading"><Loading /></el-icon>
                {{ parseStates[att.id]?.progress || '解析中...' }}
              </span>
              <span v-else-if="parseStates[att.id]?.status === 'completed'" class="att-parse-status completed" style="cursor:pointer" @click.stop="viewParseResult(att)">✅ 已解析 · 查看</span>
              <span v-else-if="parseStates[att.id]?.status === 'failed'" class="att-parse-status failed">❌ 失败</span>
            </div>
          </div>
          <el-button
            v-if="parseStates[att.id]?.status === 'completed'"
            class="att-parse-btn"
            size="small"
            type="success"
            text
            @click.stop="viewParseResult(att)"
          >
            查看
          </el-button>
          <el-button
            v-else-if="canParse(att) && (!parseStates[att.id] || parseStates[att.id]?.status === 'idle' || parseStates[att.id]?.status === 'failed')"
            class="att-parse-btn"
            size="small"
            type="primary"
            text
            @click.stop="handleParseAttachment(att)"
          >
            {{ getFileExt(att) === 'zip' ? '打开' : '解析' }}
          </el-button>
          <el-button
            v-if="parseStates[att.id]?.status === 'parsing' || parseStates[att.id]?.status === 'uploading'"
            class="att-parse-btn"
            size="small"
            type="info"
            text
            disabled
          >
            <el-icon class="is-loading"><Loading /></el-icon>
          </el-button>
          <el-dropdown trigger="click" @command="(cmd) => handleAttachmentAction(cmd, att)" @click.stop>
            <el-button class="att-more-btn" size="small" text :icon="MoreFilled" @click.stop />
            <template #dropdown>
              <el-dropdown-menu>
                <el-dropdown-item command="preview">📄 预览</el-dropdown-item>
                <el-dropdown-item v-if="canParse(att)" command="parse">🔍 解析文档</el-dropdown-item>
                <el-dropdown-item v-if="parseStates[att.id]?.status === 'completed'" command="view_result">📋 查看解析结果</el-dropdown-item>
                <el-dropdown-item command="apply">📝 应用要素</el-dropdown-item>
                <el-dropdown-item command="download">⬇️ 下载</el-dropdown-item>
                <el-dropdown-item command="delete" divided>🗑️ 删除</el-dropdown-item>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
        </div>
        <el-empty v-if="attachments.length === 0" description="暂无附件，点击添加按钮上传" :image-size="80" />
      </div>
    </el-dialog>

    <!-- 解析结果预览弹窗 -->
    <el-dialog
      v-model="showParseResultDialog"
      :title="'📋 解析结果 - ' + (parseResultAttName || '')"
      width="1100"
      :close-on-click-modal="true"
      class="floating-panel-dialog parse-result-dialog"
      align-center
      @close="cleanupPreviewContent"
    >
      <!-- 引用模式提示条 -->
      <div v-if="referenceMode" class="citation-mode-banner">
        <span>📌 正在为要素「<b>{{ referenceModeElementName }}</b>」选择引用内容，请选中文本后选择引用方式</span>
        <el-button size="small" text @click="exitReferenceMode">✕ 退出</el-button>
      </div>
      <div class="parse-result-toolbar">
        <span class="parse-result-info">{{ parseResultContent ? `共 ${parseResultContent.length} 字` : '' }}</span>
        <div class="parse-result-actions">
          <el-button size="small" :type="parseResultViewMode === 'rendered' ? 'primary' : ''" @click="parseResultViewMode = 'rendered'">渲染</el-button>
          <el-button size="small" :type="parseResultViewMode === 'source' ? 'primary' : ''" @click="parseResultViewMode = 'source'">源码</el-button>
          <el-button v-if="parseResultPreviewAvailable" size="small" :type="parseResultViewMode === 'preview' ? 'primary' : ''" @click="switchToPreviewMode">原件</el-button>
          <el-button size="small" @click="copyParseResult">📋 复制</el-button>
        </div>
      </div>
      <div class="parse-result-content" @mouseup="onParseResultMouseUp">
        <div v-if="parseResultViewMode === 'rendered'" class="parse-result-rendered markdown-body" v-html="parseResultHtml"></div>
        <pre v-else-if="parseResultViewMode === 'source'" class="parse-result-pre">{{ parseResultSource }}</pre>
        <div v-else-if="parseResultViewMode === 'preview'" class="parse-result-preview">
          <img v-if="previewContentType === 'image'" :src="previewContentUrl" :alt="parseResultAttName" style="max-width:100%;height:auto;display:block;margin:0 auto" />
          <iframe v-else-if="previewContentType === 'pdf'" :src="previewContentUrl" style="width:100%;height:75vh;border:none" />
          <div v-else-if="previewContentType === 'html'" class="parse-result-rendered markdown-body" v-html="previewContentHtml" />
          <pre v-else-if="previewContentType === 'text'" style="margin:0;white-space:pre-wrap;word-wrap:break-word;font-size:13px;line-height:1.7;color:#1f2937">{{ previewContentText }}</pre>
          <div v-else style="text-align:center;padding:40px;color:#999">该文件类型暂不支持原件预览</div>
        </div>
      </div>

      <!-- 选中文本后的浮动引用工具栏 -->
      <div
        v-if="citationToolbar.visible"
        class="citation-toolbar"
        :style="{ top: citationToolbar.y + 'px', left: citationToolbar.x + 'px' }"
      >
        <!-- 第一步：选择引用方式 -->
        <template v-if="citationToolbar.step === 'select_action'">
          <div class="citation-toolbar-title">引用到要素：</div>
          <div class="citation-toolbar-actions">
            <el-button size="small" type="primary" @click="setCitationAction('quote')">📝 直接引用</el-button>
            <el-button size="small" @click="setCitationAction('summary')">🤖 AI 总结</el-button>
            <el-button size="small" @click="setCitationAction('table_extract')">📊 表格提取</el-button>
          </div>
        </template>
        <!-- 第二步：选择目标要素（非引用模式时） -->
        <template v-if="citationToolbar.step === 'select_element'">
          <div class="citation-toolbar-title">选择目标要素：</div>
          <div class="citation-toolbar-elements">
            <div
              v-for="elem in elements"
              :key="elem.elementKey"
              class="citation-element-item"
              @click="confirmCitation(elem)"
            >
              <span class="citation-elem-name">{{ elem.elementName }}</span>
              <el-tag size="small" type="info">{{ elem.elementType }}</el-tag>
            </div>
            <div v-if="elements.length === 0" style="padding:8px;color:#999;font-size:12px">暂无要素</div>
          </div>
        </template>
      </div>
    </el-dialog>

    <!-- ZIP 内容展示弹窗 -->
    <el-dialog
      v-model="showZipContentsDialog"
      :title="'📦 ' + (zipContentsAttName || 'ZIP 内容')"
      width="520"
      :close-on-click-modal="true"
      class="floating-panel-dialog"
      align-center
    >
      <div class="fp-toolbar">
        <span class="fp-count">共 {{ zipFileList.length }} 个文件，{{ zipFileList.filter(f => f.parseable).length }} 个可解析</span>
        <el-button
          size="small"
          type="primary"
          :disabled="zipFileList.filter(f => f.parseable && !f.parsed && !f.parsing).length === 0"
          @click="parseAllZipEntries"
        >🔍 一键全部解析</el-button>
      </div>
      <div class="fp-list">
        <div v-if="zipFileList.length === 0" style="text-align:center;padding:30px;color:#999">ZIP 包为空</div>
        <div v-for="(zf, idx) in zipFileList" :key="idx" class="fp-att-item">
          <div class="att-icon" :class="getZipEntryTypeClass(zf)">
            <span class="att-icon-text">{{ getZipEntryTypeLabel(zf) }}</span>
          </div>
          <div class="att-info">
            <div class="att-name" :title="zf.name">{{ zf.name.split('/').pop() }}</div>
            <div class="att-meta">
              <span class="att-type">{{ zf.ext || '文件' }}</span>
              <span class="att-size">{{ formatFileSize(zf.size) }}</span>
              <span v-if="zf.parsing" class="att-parse-status parsing">
                <el-icon class="is-loading"><Loading /></el-icon> 解析中...
              </span>
              <span v-else-if="zf.parsed" class="att-parse-status completed" style="cursor:pointer" @click="viewZipEntryResult(zf)">✅ 已解析 · 查看</span>
            </div>
          </div>
          <el-button
            class="att-parse-btn"
            size="small"
            text
            @click="previewZipEntry(zf)"
          >预览</el-button>
          <el-button
            v-if="zf.parsed"
            class="att-parse-btn"
            size="small"
            type="success"
            text
            @click="viewZipEntryResult(zf)"
          >查看</el-button>
          <el-button
            v-else-if="zf.parseable && !zf.parsing"
            class="att-parse-btn"
            size="small"
            type="primary"
            text
            @click="parseZipEntry(zf)"
          >解析</el-button>
          <el-button
            v-else-if="zf.parsing"
            class="att-parse-btn"
            size="small"
            type="info"
            text
            disabled
          ><el-icon class="is-loading"><Loading /></el-icon></el-button>
        </div>
      </div>
    </el-dialog>


    <!-- 引用模式：附件选择弹窗 -->
    <el-dialog
      v-model="showRefAttSelectDialog"
      title="📎 选择要引用的附件"
      width="480"
      :close-on-click-modal="true"
      class="floating-panel-dialog"
      align-center
    >
      <div class="fp-list">
        <div
          v-for="att in refAttSelectList"
          :key="att.id"
          class="fp-att-item"
          style="cursor:pointer"
          @click="onRefAttSelected(att)"
        >
          <div class="att-icon" :class="getFileTypeClass(att)">
            <span class="att-icon-text">{{ getFileTypeLabel(att) }}</span>
          </div>
          <div class="att-info">
            <div class="att-name" :title="att.displayName">{{ att.displayName }}</div>
            <div class="att-meta">
              <span class="att-type">{{ getFileTypeTag(att) }}</span>
              <span class="att-size">{{ formatFileSize(att.fileSize) }}</span>
              <span class="att-parse-status completed">✅ 已解析</span>
            </div>
          </div>
        </div>
      </div>
    </el-dialog>

    <!-- 规则管理弹窗 -->
    <el-dialog
      v-model="showRuleDialog"
      title="⚙️ 规则管理"
      width="800"
      :close-on-click-modal="true"
      class="floating-panel-dialog rule-manage-dialog"
      align-center
    >
      <div class="fp-toolbar">
        <el-input
          v-model="ruleSearchQuery"
          size="small"
          placeholder="搜索规则名称 / 要素标识..."
          clearable
          style="width: 280px"
          :prefix-icon="Search"
        />
        <el-button size="small" :icon="Plus" @click="openWorkflowForNewRule">添加规则</el-button>
        <el-button
          v-if="rules.length > 0"
          type="success"
          size="small"
          @click="handleBatchExecuteRules"
          :loading="executingRules"
        >
          批量执行
        </el-button>
        <span class="fp-count">{{ filteredRules.length }} / {{ rules.length }} 条</span>
      </div>
      <!-- 类型筛选标签栏 -->
      <div class="rule-filter-bar">
        <span
          class="rule-filter-tab"
          :class="{ active: ruleFilterType === 'all' }"
          @click="ruleFilterType = 'all'"
        >全部 <em>{{ ruleTypeStats.all || 0 }}</em></span>
        <span
          class="rule-filter-tab tab-summary"
          :class="{ active: ruleFilterType === 'summary' }"
          @click="ruleFilterType = 'summary'"
        >AI总结 <em>{{ ruleTypeStats.summary || 0 }}</em></span>
        <span
          class="rule-filter-tab tab-ai_extract"
          :class="{ active: ruleFilterType === 'ai_extract' }"
          @click="ruleFilterType = 'ai_extract'"
        >AI提取 <em>{{ ruleTypeStats.ai_extract || 0 }}</em></span>
        <span
          class="rule-filter-tab tab-table_extract"
          :class="{ active: ruleFilterType === 'table_extract' }"
          @click="ruleFilterType = 'table_extract'"
        >表格提取 <em>{{ ruleTypeStats.table_extract || 0 }}</em></span>
        <span
          class="rule-filter-tab tab-quote"
          :class="{ active: ruleFilterType === 'quote' }"
          @click="ruleFilterType = 'quote'"
        >引用 <em>{{ ruleTypeStats.quote || 0 }}</em></span>
        <span
          class="rule-filter-tab tab-use_entity_value"
          :class="{ active: ruleFilterType === 'use_entity_value' }"
          @click="ruleFilterType = 'use_entity_value'"
        >人工录入 <em>{{ ruleTypeStats.use_entity_value || 0 }}</em></span>
      </div>
      <!-- 规则列表 -->
      <div class="fp-list">
        <div
          v-for="rule in filteredRules"
          :key="rule.id"
          class="fp-rule-item"
          :class="{ expanded: expandedRuleId === rule.id }"
          @click="openWorkflowForRule(rule)"
        >
          <div class="rule-item-main">
            <span class="rule-action-badge" :class="'action-' + rule.actionType">{{ ruleActionLabel(rule.actionType) }}</span>
            <div class="rule-info">
              <div class="rule-name-row">
                <span class="rule-name">{{ rule.ruleName }}</span>
                <el-tag size="small" type="info" effect="plain" class="rule-elem-key">{{ rule.elementKey }}</el-tag>
              </div>
              <div class="rule-desc" v-if="ruleSourceSummary(rule)">来源：{{ ruleSourceSummary(rule) }}</div>
            </div>
            <div class="rule-actions">
              <el-button v-if="rule.actionType !== 'use_entity_value'" size="small" type="primary" text @click.stop="handleExecuteRule(rule)" title="执行" :loading="rule._executing">▶</el-button>
              <el-button size="small" type="danger" text :icon="Delete" @click.stop="handleDeleteRule(rule)" title="删除" />
            </div>
          </div>
          <!-- 展开详情 -->
          <div class="rule-detail" v-if="expandedRuleId === rule.id">
            <div class="rule-detail-row" v-if="rule.dslContent">
              <span class="rule-detail-label">取值规则</span>
              <span class="rule-detail-value">{{ rule.dslContent }}</span>
            </div>
            <div class="rule-detail-row" v-if="ruleInputDisplayList(rule).length > 0">
              <span class="rule-detail-label">输入来源</span>
              <div class="rule-detail-inputs">
                <span v-for="(src, idx) in ruleInputDisplayList(rule)" :key="`${rule.id}-src-${idx}`" class="rule-input-chip">
                  📎 {{ src }}
                </span>
              </div>
            </div>
            <div class="rule-detail-row" v-if="rule.lastRunStatus">
              <span class="rule-detail-label">上次执行</span>
              <el-tag size="small" :type="rule.lastRunStatus === 'success' ? 'success' : 'danger'">
                {{ rule.lastRunStatus === 'success' ? '成功' : '失败' }}
              </el-tag>
              <span v-if="rule.lastRunTime" class="rule-detail-time">{{ rule.lastRunTime }}</span>
            </div>
            <div class="rule-detail-row" v-if="rule.lastRunError">
              <span class="rule-detail-label">错误信息</span>
              <span class="rule-detail-error">{{ rule.lastRunError }}</span>
            </div>
          </div>
        </div>
        <el-empty v-if="filteredRules.length === 0" :description="ruleSearchQuery || ruleFilterType !== 'all' ? '无匹配规则' : '暂无规则，点击添加按钮创建'" :image-size="80" />
      </div>
    </el-dialog>

    <!-- 新建项目对话框 -->
    <el-dialog v-model="showNewProjectDialog" title="新建项目" width="520" :close-on-click-modal="false">
      <el-form :model="newProjectForm" label-width="100px">
        <el-form-item label="项目名称" required>
          <el-input v-model="newProjectForm.title" placeholder="请输入项目名称" maxlength="100" show-word-limit />
        </el-form-item>
        <el-form-item label="项目描述">
          <el-input v-model="newProjectForm.description" type="textarea" :rows="2" placeholder="项目描述（可选）" />
        </el-form-item>
        
        <!-- 可选：上传DOCX文档自动提取要素 -->
        <el-form-item label="导入文档">
          <div class="upload-docx-area">
            <el-upload
              ref="docxUploadRef"
              :auto-upload="false"
              :show-file-list="true"
              :limit="1"
              accept=".docx"
              :on-change="handleDocxFileChange"
              :on-remove="handleDocxFileRemove"
            >
              <template #trigger>
                <el-button type="primary" plain size="small">
                  <el-icon><Upload /></el-icon>
                  选择DOCX文件
                </el-button>
              </template>
              <template #tip>
                <div class="el-upload__tip">
                  可选：上传DOCX文件自动解析并提取要素
                </div>
              </template>
            </el-upload>
          </div>
        </el-form-item>
        
        <!-- 解析进度 -->
        <el-form-item v-if="docxParseProgress > 0" label="解析进度">
          <el-progress :percentage="docxParseProgress" :status="docxParseStatus" />
          <div class="parse-message">{{ docxParseMessage }}</div>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="handleCancelNewProject">取消</el-button>
        <el-button 
          type="primary" 
          @click="handleCreateProject" 
          :disabled="!newProjectForm.title.trim()" 
          :loading="creatingProject"
        >
          {{ newProjectForm.docxFile ? '创建并解析' : '创建' }}
        </el-button>
      </template>
    </el-dialog>

    <!-- 添加要素对话框 -->
    <el-dialog v-model="showAddElementDialog" title="添加要素" width="500">
      <el-form :model="newElementForm" label-width="100px">
        <el-form-item label="要素名称" required>
          <el-input v-model="newElementForm.elementName" placeholder="如：项目编号" />
        </el-form-item>
        <el-form-item label="要素标识" required>
          <el-input v-model="newElementForm.elementKey" placeholder="如：basicInfo.projectCode" />
        </el-form-item>
        <el-form-item label="数据类型">
          <el-select v-model="newElementForm.dataType" style="width: 100%">
            <el-option label="文本" value="text" />
            <el-option label="数字" value="number" />
            <el-option label="日期" value="date" />
            <el-option label="金额" value="money" />
          </el-select>
        </el-form-item>
        <el-form-item label="描述">
          <el-input v-model="newElementForm.description" type="textarea" :rows="2" placeholder="要素描述" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showAddElementDialog = false">取消</el-button>
        <el-button type="primary" @click="handleAddElement" :disabled="!newElementForm.elementName || !newElementForm.elementKey">添加</el-button>
      </template>
    </el-dialog>

    <!-- 添加规则对话框 -->
    <el-dialog v-model="showNewRuleDialog" title="添加规则" width="600">
      <el-form :model="newRuleForm" label-width="100px">
        <el-form-item label="规则名称" required>
          <el-input v-model="newRuleForm.ruleName" placeholder="如：项目编号-直接引用实体" />
        </el-form-item>
        <el-form-item label="规则类型" required>
          <el-select v-model="newRuleForm.ruleType" style="width: 100%">
            <el-option label="直接引用实体" value="direct_entity" />
            <el-option label="AI 提取" value="ai_extract" />
            <el-option label="AI 总结" value="summary" />
            <el-option label="表格提取" value="table_extract" />
            <el-option label="固定值" value="fixed_value" />
            <el-option label="计算公式" value="formula" />
          </el-select>
        </el-form-item>
        <el-form-item label="目标要素">
          <el-select v-model="newRuleForm.targetElementKey" style="width: 100%" placeholder="选择要填充的要素">
            <el-option v-for="elem in elements" :key="elem.elementKey" :label="elem.elementName" :value="elem.elementKey" />
          </el-select>
        </el-form-item>
        <el-form-item label="来源附件" v-if="['ai_extract', 'summary', 'table_extract', 'direct_entity'].includes(newRuleForm.ruleType)">
          <el-select v-model="newRuleForm.sourceAttachmentId" style="width: 100%" placeholder="选择来源附件" clearable>
            <el-option v-for="att in attachments" :key="att.id" :label="att.fileName" :value="att.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="内容定位" v-if="['ai_extract', 'summary', 'table_extract'].includes(newRuleForm.ruleType)">
          <el-select v-model="newRuleForm.locatorType" style="width: 100%" placeholder="选择内容定位方式">
            <el-option label="全文" value="full_text" />
            <el-option label="章节定位" value="chapter" />
            <el-option label="评审代码定位" value="review_code" />
            <el-option label="表格定位" value="table" />
          </el-select>
        </el-form-item>
        <el-form-item label="章节标题" v-if="newRuleForm.locatorType === 'chapter'">
          <el-input v-model="newRuleForm.chapterTitle" placeholder="如：一、工作目的" />
        </el-form-item>
        <el-form-item label="评审代码" v-if="newRuleForm.locatorType === 'review_code'">
          <el-input v-model="newRuleForm.reviewCode" placeholder="如：5.1.5" />
        </el-form-item>
        <el-form-item label="表格选择器" v-if="newRuleForm.locatorType === 'table'">
          <el-input v-model="newRuleForm.tableSelector" placeholder="如：核心要素评审情况记录表" />
        </el-form-item>
        <el-form-item label="AI 提示词" v-if="['ai_extract', 'summary'].includes(newRuleForm.ruleType)">
          <el-input 
            v-model="newRuleForm.prompt" 
            type="textarea" 
            :rows="3" 
            placeholder="如：从原文提取评审对象公司的全称，只输出公司名称" 
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showNewRuleDialog = false">取消</el-button>
        <el-button type="primary" @click="handleCreateRule" :disabled="!newRuleForm.ruleName">创建</el-button>
      </template>
    </el-dialog>

    <!-- 规则引擎数据弹窗 -->
    <el-dialog v-model="showRuleEngineDialog" :title="pendingExecuteRule ? '执行规则' : '批量执行规则'" width="900">
      <div class="rule-engine-preview">
        <p class="rule-engine-desc">{{ pendingExecuteRule ? '即将执行以下规则：' : '以下是非人工录入要素的规则信息，可用于规则引擎处理：' }}</p>
        <div class="rule-engine-stats" v-if="!pendingExecuteRule">
          <span>共 <strong>{{ ruleEngineData.length }}</strong> 条自动规则</span>
        </div>
        <div class="rule-engine-list">
          <div 
            v-for="rule in displayRulesForEngine" 
            :key="rule.id" 
            class="rule-engine-item"
          >
            <div class="rule-engine-header">
              <span class="rule-engine-name">{{ rule.ruleName }}</span>
              <el-tag size="small" :type="getActionTypeTagType(rule.actionType)">{{ rule.actionType }}</el-tag>
              <el-tag size="small" type="info">{{ rule.ruleType }}</el-tag>
            </div>
            <div class="rule-engine-element">
              <span class="label">目标要素：</span>
              <code>{{ rule.elementKey }}</code>
            </div>
            <div class="rule-engine-inputs" v-if="rule.inputs && rule.inputs.length > 0">
              <span class="label">输入来源：</span>
              <div class="input-list">
                <div v-for="(inp, idx) in rule.inputs" :key="idx" class="input-item">
                  <span class="input-source">📎 {{ inp.sourceName || inp.inputName }}</span>
                  <span v-if="inp.entryPath" class="input-entry">→ {{ inp.entryPath }}</span>
                  <div v-if="inp.sourceText" class="input-text">
                    <span class="text-label">引用文本：</span>
                    <span class="text-content">{{ inp.sourceText.slice(0, 200) }}{{ inp.sourceText.length > 200 ? '...' : '' }}</span>
                  </div>
                </div>
              </div>
            </div>
            <div class="rule-engine-source" v-else-if="rule.sourceText">
              <span class="label">引用文本：</span>
              <span class="source-text">{{ (rule.sourceText || '').slice(0, 200) }}{{ (rule.sourceText || '').length > 200 ? '...' : '' }}</span>
            </div>
            <div class="rule-engine-code" v-if="rule.reviewCode">
              <span class="label">评审代码：</span>
              <code>{{ rule.reviewCode }}</code>
            </div>
            <div class="rule-engine-desc-text" v-if="rule.description">
              <span class="label">描述：</span>
              <span>{{ rule.description }}</span>
            </div>
          </div>
        </div>
        <div class="rule-engine-json">
          <div class="json-header">
            <span class="json-title">JSON 数据</span>
            <el-button size="small" @click="copyRuleEngineJson">复制</el-button>
          </div>
          <pre class="json-content">{{ JSON.stringify(ruleEngineAdaptedData, null, 2) }}</pre>
        </div>
      </div>
      <template #footer>
        <el-button @click="showRuleEngineDialog = false">取消</el-button>
        <el-button type="primary" @click="handleConfirmExecute" :loading="executingRules">
          {{ pendingExecuteRule ? '确认执行' : `确认执行 (${ruleEngineData.length} 条规则)` }}
        </el-button>
      </template>
    </el-dialog>

    <!-- 规则工作流弹窗 -->
    <el-dialog
      v-model="showRuleWorkflow"
      fullscreen
      :close-on-click-modal="false"
      :show-close="false"
      class="rule-workflow-dialog"
    >
      <template #header="{ }">
        <span style="display: none;"></span>
      </template>
      <RuleWorkflow
        v-if="showRuleWorkflow && currentProjectId"
        :key="workflowTargetRule?.id || 'new'"
        :project-id="currentProjectId"
        :attachments="attachments"
        :elements="elements"
        :rules="rules"
        :target-rule="workflowTargetRule"
        :target-element="workflowTargetElement"
        :workflow-title="workflowTargetRule ? `编辑规则 - ${workflowTargetElement?.elementName || workflowTargetRule.elementKey}` : '新建规则'"
        @save="handleWorkflowSave"
        @close="showRuleWorkflow = false"
      />
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Plus, Delete, Search, Loading, Check, CopyDocument, MoreFilled, List, Folder, Document, Grid, Setting, Paperclip, Upload } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { projectApi, elementApi, valueApi, attachmentApi, ruleApi, parseApi, extractApi, entityApi } from '@/api'
import { marked } from 'marked'
import JSZip from 'jszip'
import { useTaskCenterStore } from '@/stores/taskCenter'
import RuleWorkflow from '@/components/workflow/RuleWorkflow.vue'

const router = useRouter()
const route = useRoute()
const taskCenterStore = useTaskCenterStore()
const taskRunningCount = computed(() => taskCenterStore.runningCount)

const currentProjectId = ref(null)
const hasActiveProject = computed(() => !!currentProjectId.value)
const userName = computed(() => localStorage.getItem('username') || '用户')
const greetingText = computed(() => {
  const hour = new Date().getHours()
  if (hour < 6) return '凌晨好'
  if (hour < 9) return '早上好'
  if (hour < 12) return '上午好'
  if (hour < 14) return '中午好'
  if (hour < 18) return '下午好'
  if (hour < 22) return '晚上好'
  return '夜深了'
})

const projectTitle = ref('')
const viewMode = ref('document')
const saved = ref(true)
const editorRef = ref(null)
const loading = ref(false)
const leftPanelWidth = ref(300)
const rightPanelWidth = ref(340)
const isResizing = ref(false)
const resizeType = ref('')
const leftPanelTab = ref('projects')
const sidebarShowAll = ref(false)

const recentActivities = computed(() => {
  // 基于项目列表生成最近操作 + 额外 mock 数据
  const acts = []
  
  // 从项目列表生成
  for (const p of projects.value.slice(0, 3)) {
    acts.push({
      text: `打开项目「${p.title}」`,
      source: `@${userName.value}`,
      time: formatTime(p.updatedAt || p.createdAt)
    })
  }
  
  // 额外 mock 数据 - 模拟各种操作类型
  const mockActivities = [
    { text: '修改要素「评审对象」的值', source: '@张三', time: '10分钟前' },
    { text: '执行规则「评审得分提取」', source: '@系统', time: '15分钟前' },
    { text: '上传附件「核心要素评审记录表.docx」', source: '@李四', time: '30分钟前' },
    { text: '新建规则「评审期自动计算」', source: '@张三', time: '1小时前' },
    { text: '导出报告「成都院复审报告」', source: '@王五', time: '2小时前' },
    { text: '修改要素「评审结论级别」的值', source: '@李四', time: '3小时前' },
    { text: '删除附件「旧版评审表.xlsx」', source: '@张三', time: '昨天 16:30' },
    { text: '创建项目「西南院标准化复审」', source: '@管理员', time: '昨天 10:15' },
  ]
  
  return [...acts, ...mockActivities].slice(0, 10)
})

const projects = ref([])
const loadingProjects = ref(false)
const projectSearchKeyword = ref('')
const elements = ref([])
const values = ref([])
const attachments = ref([])
const rules = ref([])
const selectedAttachment = ref(null)
const executingRules = ref(false)

// AI 助手
const aiMessages = ref([])
const aiInputText = ref('')
const aiMessagesRef = ref(null)

// 已填充的要素值（用于右侧标签云）
const filledValues = computed(() => {
  const result = []
  for (const elem of elements.value) {
    const elemVals = values.value.filter(v => stripValueKeyPrefix(v.elementKey) === elem.elementKey)
    for (const val of elemVals) {
      if (val.valueText && val.valueText.length > 0) {
        result.push({ valueId: val.valueId, valueText: val.valueText, elementName: elem.elementName, elementKey: elem.elementKey })
      }
    }
  }
  return result
})

// 要素搜索和分组
const elementSearchVisible = ref(false)
const elementSearchQuery = ref('')
const elementGroupExpanded = reactive({})

const NAMESPACE_LABELS = {
  'project': '项目信息',
  'basicInfo': '基本信息',
  'review': '评审信息',
  'score': '评分信息',
  '+': '扩展要素',
  'other': '其他'
}

const groupedElements = computed(() => {
  const groups = {}
  const query = elementSearchQuery.value.toLowerCase()
  
  for (const elem of elements.value) {
    // 搜索过滤
    if (query && !elem.elementName.toLowerCase().includes(query) && !elem.elementKey.toLowerCase().includes(query)) {
      continue
    }
    
    // 获取命名空间
    let namespace = 'other'
    if (elem.elementKey.startsWith('+')) {
      namespace = '+'
    } else if (elem.elementKey.includes('.')) {
      namespace = elem.elementKey.split('.')[0]
    }
    
    if (!groups[namespace]) {
      groups[namespace] = {
        namespace,
        label: NAMESPACE_LABELS[namespace] || namespace,
        items: []
      }
      // 默认展开第一个分组
      if (elementGroupExpanded[namespace] === undefined) {
        elementGroupExpanded[namespace] = Object.keys(groups).length === 1
      }
    }
    
    // 查找对应的值
    const val = values.value.find(v => stripValueKeyPrefix(v.elementKey) === elem.elementKey)
    
    groups[namespace].items.push({
      elementKey: elem.elementKey,
      elementName: elem.elementName,
      elementType: elem.elementType || 'text',
      hasValue: !!(val?.valueText),
      valueText: val?.valueText || ''
    })
  }
  
  // 按顺序排列
  const order = ['project', 'basicInfo', 'review', 'score', '+', 'other']
  return order.filter(ns => groups[ns]).map(ns => groups[ns])
})

function toggleElementGroup(namespace) {
  elementGroupExpanded[namespace] = !elementGroupExpanded[namespace]
}

function truncateValue(text, maxLen = 20) {
  if (!text) return ''
  const s = String(text).replace(/\n/g, ' ').trim()
  return s.length > maxLen ? s.slice(0, maxLen) + '...' : s
}

function scrollToElement(item) {
  // 查找文档中对应的高亮元素并滚动到视图
  const docPaper = docPaperRef.value
  if (!docPaper) return
  
  // 高亮系统使用 data-elem-key 属性
  const selector = `[data-elem-key="${item.elementKey}"]`
  const el = docPaper.querySelector(selector)
  if (el) {
    // 使用 IntersectionObserver 监听元素进入视口后再闪烁
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          observer.disconnect()
          // 元素已在视口中，延迟一点再闪烁确保滚动稳定
          setTimeout(() => {
            el.classList.add('elem-flash')
            setTimeout(() => el.classList.remove('elem-flash'), 1500)
            // 触发点击以打开弹窗
            el.click()
          }, 100)
        }
      })
    }, { threshold: 0.5 })
    
    observer.observe(el)
    el.scrollIntoView({ behavior: 'smooth', block: 'center' })
    
    // 超时保护：如果 3 秒内没有触发，强制执行
    setTimeout(() => {
      observer.disconnect()
    }, 3000)
  } else {
    // 如果没有找到高亮元素，尝试在文档中搜索值文本
    if (item.valueText) {
      const textToFind = item.valueText.slice(0, 50) // 取前50个字符搜索
      const walker = document.createTreeWalker(docPaper, NodeFilter.SHOW_TEXT, null, false)
      let node
      while (node = walker.nextNode()) {
        if (node.textContent.includes(textToFind)) {
          const range = document.createRange()
          range.selectNodeContents(node)
          const rect = range.getBoundingClientRect()
          node.parentElement?.scrollIntoView({ behavior: 'smooth', block: 'center' })
          break
        }
      }
    }
    ElMessage.info(`未找到要素「${item.elementName}」的高亮位置`)
  }
}

// 文档预览状态
const docContent = ref(null)
const docLoading = ref(false)
const docHtml = ref('')
const docTemplateHtml = ref('')
const docPaperRef = ref(null)
const highlightEnabled = ref(true)
const elementHighlightCount = ref(0)
const highlightPopover = reactive({
  visible: false, x: 0, y: 0,
  elementKey: '', fullElementKey: '', elementName: '', currentValue: '', originalValue: '', valueId: null
})

// 附件引用系统
const citationToolbar = reactive({
  visible: false, x: 0, y: 0,
  selectedText: '',
  step: 'select_action', // 'select_action' | 'select_element'
  actionType: '',        // 'quote' | 'summary' | 'table_extract'
})
const showRefAttSelectDialog = ref(false)   // 引用模式下的附件选择弹窗
const refAttSelectList = ref([])            // 可选附件列表
const refAttSelectElemKey = ref('')         // 暂存目标要素 key
const refAttSelectElemName = ref('')        // 暂存目标要素名
const referenceMode = ref(false)            // 是否处于「从要素引用附件」模式
const referenceModeElementKey = ref('')     // 引用模式下锁定的目标要素 key
const referenceModeElementName = ref('')    // 引用模式下锁定的目标要素名
const referenceModeAttId = ref(null)        // 引用来源的附件节点 ID
const referenceModeAttName = ref('')        // 引用来源的附件名

const showNewProjectDialog = ref(false)
const showAddElementDialog = ref(false)
const showNewRuleDialog = ref(false)
const showAttachmentDialog = ref(false)
const showRuleDialog = ref(false)
const showRuleWorkflow = ref(false)
const workflowTargetRule = ref(null)  // 当前编辑的规则（null 表示新建）
const workflowTargetElement = ref(null)  // 当前编辑的目标要素
const ruleSearchQuery = ref('')
const ruleFilterType = ref('all')
const expandedRuleId = ref(null)

const filteredRules = computed(() => {
  let list = rules.value
  if (ruleFilterType.value !== 'all') {
    list = list.filter(r => r.actionType === ruleFilterType.value)
  }
  const q = ruleSearchQuery.value.trim().toLowerCase()
  if (q) {
    list = list.filter(r =>
      (r.ruleName || '').toLowerCase().includes(q) ||
      (r.elementKey || '').toLowerCase().includes(q) ||
      (r.description || '').toLowerCase().includes(q) ||
      ruleSourceSummary(r).toLowerCase().includes(q)
    )
  }
  return list
})

const ruleTypeStats = computed(() => {
  const stats = { all: rules.value.length }
  for (const r of rules.value) {
    const t = r.actionType || 'unknown'
    stats[t] = (stats[t] || 0) + 1
  }
  return stats
})

// 规则引擎准备数据：获取非人工录入要素的规则信息
const ruleEngineData = computed(() => {
  // 过滤掉人工录入类型的规则
  const autoRules = rules.value.filter(r => r.actionType !== 'use_entity_value')
  
  return autoRules.map(rule => {
    // 获取输入文本
    const inputs = Array.isArray(rule.inputs) ? rule.inputs : []
    const inputTexts = inputs.map(inp => {
      return {
        sourceName: inp.sourceName || inp.inputName || '',
        sourceText: inp.sourceText || '',
        entryPath: inp.entryPath || '',
        inputType: inp.inputType || ''
      }
    }).filter(inp => inp.sourceName || inp.sourceText)
    
    // 从 actionConfig 中提取额外信息
    let actionConfigData = {}
    try {
      actionConfigData = typeof rule.actionConfig === 'string' 
        ? JSON.parse(rule.actionConfig) 
        : (rule.actionConfig || {})
    } catch (e) {}
    
    return {
      id: rule.id,
      ruleName: rule.ruleName,
      elementKey: rule.elementKey,
      ruleType: rule.ruleType,
      actionType: rule.actionType,
      actionConfig: rule.actionConfig,
      description: rule.description,
      inputs: inputTexts,
      sourceText: actionConfigData.sourceText || inputTexts[0]?.sourceText || '',
      reviewCode: actionConfigData.description?.match(/评审代码\s*(\d+\.\d+(?:\.\d+)*)/)?.[1] || ''
    }
  })
})

// 显示规则引擎数据弹窗
const showRuleEngineDialog = ref(false)
// 当前要执行的单条规则（用于弹窗展示）
const pendingExecuteRule = ref(null)

// 弹窗中显示的规则列表（单条或批量）
const displayRulesForEngine = computed(() => {
  if (pendingExecuteRule.value) {
    // 单条规则模式：转换为统一格式
    const rule = pendingExecuteRule.value
    const inputs = Array.isArray(rule.inputs) ? rule.inputs : []
    let actionConfigData = {}
    try {
      actionConfigData = typeof rule.actionConfig === 'string' 
        ? JSON.parse(rule.actionConfig) 
        : (rule.actionConfig || {})
    } catch (e) {}
    
    return [{
      id: rule.id,
      ruleName: rule.ruleName,
      elementKey: rule.elementKey,
      ruleType: rule.ruleType,
      actionType: rule.actionType,
      actionConfig: rule.actionConfig,
      description: rule.description,
      inputs: inputs,
      sourceText: actionConfigData.sourceText || '',
      reviewCode: actionConfigData.description?.match(/评审代码\s*(\d+\.\d+(?:\.\d+)*)/)?.[1] || ''
    }]
  }
  // 批量模式
  return ruleEngineData.value
})

// 生成规则引擎适配格式的数据
const ruleEngineAdaptedData = computed(() => {
  return displayRulesForEngine.value.map(rule => {
    // 提取输入资源 ID 列表
    const inputs = Array.isArray(rule.inputs) ? rule.inputs : []
    const resourceIds = inputs
      .filter(inp => inp.sourceNodeId)
      .map(inp => ({
        var_name: `resource_${inp.sourceNodeId}_id`,
        id: inp.sourceNodeId,
        name: inp.sourceName || inp.inputName || '',
        entry_path: inp.entryPath || null
      }))
    
    // 从 actionConfig 中提取详细配置
    let actionConfigData = {}
    try {
      actionConfigData = typeof rule.actionConfig === 'string' 
        ? JSON.parse(rule.actionConfig) 
        : (rule.actionConfig || {})
    } catch (e) {}
    
    // 提取 prompt（AI 提示词）
    let prompt = actionConfigData.prompt || ''
    const configDesc = actionConfigData.description || ''
    if (!prompt) {
      if (rule.actionType === 'summary') {
        prompt = configDesc || `从原文生成${rule.ruleName}的摘要描述`
      } else if (rule.actionType === 'ai_extract') {
        prompt = configDesc || `从原文提取${rule.ruleName}相关信息`
      } else if (rule.actionType === 'table_extract') {
        prompt = configDesc || `从表格中提取${rule.ruleName}数据`
      } else if (rule.actionType === 'quote') {
        prompt = configDesc || `直接引用原文中的${rule.ruleName}内容`
      }
    }
    
    // 确定函数类型
    let funcType = 'export_resource'  // 默认直接导出
    if (rule.actionType === 'summary' || rule.actionType === 'ai_extract') {
      funcType = 'ai_assistant'
    } else if (rule.actionType === 'table_extract') {
      funcType = 'table_extract'
    } else if (rule.actionType === 'quote') {
      funcType = 'export_resource'
    }
    
    // 提取引用的原文内容
    const sourceText = actionConfigData.sourceText || rule.sourceText || ''
    
    // 提取评审代码
    const reviewCode = rule.reviewCode || 
      actionConfigData.reviewCode ||
      (configDesc.match(/评审代码\s*(\d+\.\d+(?:\.\d+)*)/)?.[1]) || null
    
    // 内容定位信息
    const contentLocator = {
      type: actionConfigData.locatorType || 'full_text',
      chapter_title: actionConfigData.chapterTitle || null,
      review_code: reviewCode,
      table_selector: actionConfigData.tableSelector || null
    }
    
    // 根据定位类型自动推断
    if (!actionConfigData.locatorType) {
      if (reviewCode) {
        contentLocator.type = 'review_code'
      } else if (rule.actionType === 'table_extract') {
        contentLocator.type = 'table'
      }
    }
    
    return {
      // 规则基本信息
      rule_id: rule.id,
      rule_name: rule.ruleName,
      element_key: rule.elementKey,
      
      // 规则引擎适配参数
      func_type: funcType,
      action_type: rule.actionType,
      
      // 输入资源（对应 *_ch_id 等参数）
      resource_ids: resourceIds,
      
      // 内容定位方式
      content_locator: contentLocator,
      
      // AI 提示词（对应 prompt 参数）
      prompt: prompt,
      
      // 引用的原文内容（用于定位和上下文）
      source_text: sourceText,
      
      // 原始描述（来源说明）
      source_desc: rule.description
    }
  })
})

// 统一的确认执行函数
function handleConfirmExecute() {
  if (pendingExecuteRule.value) {
    confirmExecuteSingleRule()
  } else {
    confirmExecuteRules()
  }
}

function toggleRuleExpand(ruleId) {
  expandedRuleId.value = expandedRuleId.value === ruleId ? null : ruleId
}

// 打开工作流编辑器 - 新建规则
function openWorkflowForNewRule() {
  workflowTargetRule.value = null
  workflowTargetElement.value = null
  showRuleDialog.value = false
  showRuleWorkflow.value = true
}

// 打开工作流编辑器 - 编辑现有规则
async function openWorkflowForRule(rule) {
  try {
    // 获取规则完整详情（包含 inputs）
    const fullRule = await ruleApi.getById(rule.id)
    workflowTargetRule.value = fullRule
    // 找到对应的要素
    const elem = elements.value.find(e => e.elementKey === fullRule.elementKey)
    workflowTargetElement.value = elem || { elementKey: fullRule.elementKey, elementName: fullRule.elementKey }
    showRuleDialog.value = false
    showRuleWorkflow.value = true
  } catch (e) {
    console.error('获取规则详情失败:', e)
    // 降级使用列表中的规则数据
    workflowTargetRule.value = rule
    const elem = elements.value.find(e => e.elementKey === rule.elementKey)
    workflowTargetElement.value = elem || { elementKey: rule.elementKey, elementName: rule.elementKey }
    showRuleDialog.value = false
    showRuleWorkflow.value = true
  }
}

// 从弹窗中打开规则工作流编辑器
async function openRuleWorkflow(rule) {
  // 关闭弹窗
  highlightPopover.visible = false
  // 复用现有的打开工作流函数
  await openWorkflowForRule(rule)
}

function normalizeRuleSourceName(name) {
  const s = String(name || '').trim()
  if (!s) return ''
  return s.replace(/^来源[:：]\s*/i, '')
}

function formatInputSource(inp) {
  const sourceName = inp?.sourceName || inp?.inputName || ''
  const entryPath = inp?.entryPath
  if (entryPath) {
    // 有 entryPath 时，显示 "附件名 → 文件名"
    const fileName = entryPath.split('/').pop()
    return `${sourceName} → ${fileName}`
  }
  return sourceName
}

function getInputSourceText(rule) {
  // 从规则的 inputs 中获取 sourceText
  const inputs = Array.isArray(rule?.inputs) ? rule.inputs : []
  for (const inp of inputs) {
    if (inp.sourceText) return inp.sourceText
  }
  return ''
}

async function openSourceInViewer(inp, rule = null) {
  console.log('[openSourceInViewer] 开始溯源定位')
  console.log('[openSourceInViewer] inp:', inp)
  console.log('[openSourceInViewer] rule:', rule)
  
  // 关闭弹窗
  highlightPopover.visible = false
  
  const attId = inp.sourceNodeId
  const entryPath = inp.entryPath
  let sourceText = inp.sourceText || ''
  
  console.log('[openSourceInViewer] attId:', attId, 'entryPath:', entryPath, 'sourceText:', sourceText)
  
  // 如果没有 sourceText，尝试从规则的各个字段中提取评审代码（如 5.1.5）
  if (!sourceText && rule) {
    // 尝试从 description、actionConfig、ruleName 等字段提取
    const searchFields = [
      rule.description,
      rule.actionConfig,
      rule.ruleName
    ].filter(Boolean).join(' ')
    console.log('[openSourceInViewer] 尝试从规则字段提取评审代码:', searchFields)
    
    // 匹配 "评审代码5.1.5" 或 "代码5.1.5" 或直接匹配 "5.1.5" 格式
    let codeMatch = searchFields.match(/评审代码\s*(\d+\.\d+(?:\.\d+)*)/i)
    if (!codeMatch) {
      codeMatch = searchFields.match(/代码\s*(\d+\.\d+(?:\.\d+)*)/i)
    }
    if (!codeMatch) {
      // 尝试从 actionConfig JSON 中提取
      try {
        const cfg = typeof rule.actionConfig === 'string' ? JSON.parse(rule.actionConfig) : rule.actionConfig
        if (cfg?.reviewCode) {
          sourceText = cfg.reviewCode
          console.log('[openSourceInViewer] 从 actionConfig.reviewCode 提取:', sourceText)
        } else if (cfg?.sourceText) {
          sourceText = cfg.sourceText
          console.log('[openSourceInViewer] 从 actionConfig.sourceText 提取:', sourceText)
        }
      } catch (e) {}
    }
    if (codeMatch) {
      sourceText = codeMatch[1]
      console.log('[openSourceInViewer] 提取到评审代码:', sourceText)
    }
    
    // 如果还是没有，尝试根据 ruleName 匹配评审代码（核心要素评审表的项目名称）
    if (!sourceText && rule.ruleName) {
      const ruleNameToCode = {
        '安全文化': '5.1.5',
        '安全文化建设': '5.1.5',
        '安全投入': '5.1.4',
        '安全生产投入': '5.1.4',
        '目标职责': '5.1',
        '目标': '5.1.1',
        '目标制定': '5.1.1.1',
        '目标落实': '5.1.1.2',
        '目标考核': '5.1.1.3',
        '机构和职责': '5.1.2',
        '机构设置': '5.1.2.1',
        '全员参与': '5.1.3',
        '信息化建设': '5.1.6',
        '安全生产信息化建设': '5.1.6',
        '制度化管理': '5.2',
        '法规标准识别': '5.2.1',
        '规章制度': '5.2.2',
        '操作规程': '5.2.3',
        '评估和修订': '5.2.4',
        '文档管理': '5.2.5',
        '设备设施管理': '5.4.1',
        '设备设施': '5.4.1',
      }
      const code = ruleNameToCode[rule.ruleName]
      if (code) {
        sourceText = code
        console.log('[openSourceInViewer] 根据 ruleName 映射到评审代码:', rule.ruleName, '->', code)
      }
    }
  }
  
  // 查找附件（兼容 id 类型差异）
  const att = attachments.value.find(a => String(a.id) === String(attId))
  console.log('[openSourceInViewer] 找到附件:', att, '| attachments:', attachments.value.map(a => ({ id: a.id, type: typeof a.id })))
  if (!att) {
    ElMessage.warning('未找到来源附件，sourceNodeId=' + attId)
    return
  }
  
  // 设置高亮文本（用于在查看器中高亮）
  highlightSourceText.value = sourceText || ''
  console.log('[openSourceInViewer] 设置 highlightSourceText:', highlightSourceText.value)
  
  // 判断是否是 ZIP 文件
  const ext = (att.fileType || '').toLowerCase()
  if (ext === 'zip' && entryPath) {
    // 打开 ZIP 内容查看器，并定位到指定文件
    await handleZipAttachment(att)
    // 等待 ZIP 内容加载完成后，自动打开指定的 entry
    setTimeout(() => {
      const fileName = entryPath.split('/').pop()
      const zf = zipFileList.value.find(f => f.name === entryPath || f.name.endsWith('/' + fileName) || f.name.endsWith(fileName))
      if (zf && zf.parsed) {
        viewZipEntryResult(zf)
      } else if (zf) {
        // 如果还没解析，先解析
        parseZipEntry(zf)
      }
    }, 800)
  } else {
    // 直接打开附件查看器
    const state = parseStates[att.id]
    console.log('[openSourceInViewer] 附件解析状态:', state?.status)
    if (state?.status === 'completed') {
      console.log('[openSourceInViewer] 调用 viewParseResult')
      viewParseResult(att)
    } else {
      // 先解析再查看
      console.log('[openSourceInViewer] 附件未解析，调用 handleParseAttachment')
      await handleParseAttachment(att)
    }
  }
}

function ruleSourceFromActionConfig(rule) {
  if (!rule.actionConfig) return ''
  try {
    const cfg = typeof rule.actionConfig === 'string' ? JSON.parse(rule.actionConfig) : rule.actionConfig
    const zipName = normalizeRuleSourceName(cfg.zipName || cfg.zipFileName || cfg.archiveName)
    const entryName = normalizeRuleSourceName(cfg.zipEntryName || cfg.entryName || cfg.fileName || cfg.attachmentName)
    if (zipName && entryName && zipName !== entryName) return `${zipName}/${entryName}`
    return entryName || zipName || ''
  } catch (_) {
    return ''
  }
}

function ruleInputDisplayList(rule) {
  const list = []
  const inputs = Array.isArray(rule?.inputs) ? rule.inputs : []
  for (const inp of inputs) {
    // 优先使用 entryPath（ZIP内文件路径），否则使用 inputName/sourceName
    const entryPath = inp?.entryPath
    const inputName = normalizeRuleSourceName(inp?.inputName || inp?.sourceName || '')
    
    if (entryPath) {
      // 有 entryPath 时，显示 "附件名 → 文件名"
      const fileName = entryPath.split('/').pop()
      const value = inputName ? `${inputName} → ${fileName}` : fileName
      if (value && !list.includes(value)) list.push(value)
    } else if (inputName) {
      if (!list.includes(inputName)) list.push(inputName)
    }
  }
  return list
}

function ruleSourceSummary(rule) {
  const fromInputs = ruleInputDisplayList(rule)
  if (fromInputs.length) return fromInputs.join('、')

  const fromCfg = ruleSourceFromActionConfig(rule)
  if (fromCfg) return fromCfg

  const desc = normalizeRuleSourceName(rule?.description)
  if (desc.startsWith('从附件「')) {
    const m = desc.match(/从附件「([^」]+)」/)
    if (m?.[1]) return m[1]
  }
  if (desc.startsWith('来源：') || desc.startsWith('来源:')) {
    return desc.replace(/^来源[:：]\s*/, '')
  }
  return desc
}
// 附件解析状态: { [attachmentId]: { status: 'idle'|'uploading'|'parsing'|'completed'|'failed', progress: '', markdown: '' } }
const parseStates = reactive({})
const highlightSourceText = ref('') // 用于在附件查看器中高亮的来源文本
const showParseResultDialog = ref(false)
const parseResultAttName = ref('')
const parseResultContent = ref('')
const parseResultViewMode = ref('rendered')
const parseResultIsHtml = ref(false)
const parseResultHtml = computed(() => {
  if (!parseResultContent.value) return ''
  let html = ''
  // DOCX 解析结果已经是 HTML，直接使用
  if (parseResultIsHtml.value) {
    html = parseResultContent.value
  } else {
    // PDF/图片解析结果是 markdown，用 marked 渲染
    try {
      html = marked(parseResultContent.value)
    } catch (e) {
      html = `<pre>${parseResultContent.value}</pre>`
    }
  }
  // 如果有高亮文本，在 HTML 中高亮显示（评审代码如 5.1.5 长度为5，所以用 >= 3）
  if (highlightSourceText.value && highlightSourceText.value.length >= 3) {
    html = highlightSourceInHtml(html, highlightSourceText.value)
  }
  return html
})

// 智能高亮来源文本
function highlightSourceInHtml(html, sourceText) {
  console.log('[highlightSourceInHtml] 开始高亮, sourceText:', sourceText, 'html长度:', html?.length)
  if (!sourceText || sourceText.length < 3) {
    console.log('[highlightSourceInHtml] sourceText 太短，跳过')
    return html
  }
  
  // 0. 检测评审代码模式（如 5.1.5, 5.1.4.1 等），高亮整个表格行
  const codeMatch = sourceText.match(/\b(\d+\.\d+(?:\.\d+)*)\b/)
  console.log('[highlightSourceInHtml] 评审代码匹配结果:', codeMatch)
  if (codeMatch) {
    const code = codeMatch[1]
    console.log('[highlightSourceInHtml] 检测到评审代码:', code)
    
    // 直接高亮评审代码本身（更简单可靠的方式）
    const codeEscaped = code.replace(/\./g, '\\.')
    // 匹配 >5.1.5< 格式（在标签之间的评审代码）
    const codeRegex = new RegExp(`(>[^<]*?)(${codeEscaped})([^<]*?<)`, 'gi')
    if (codeRegex.test(html)) {
      console.log('[highlightSourceInHtml] 高亮评审代码')
      return html.replace(new RegExp(`(>[^<]*?)(${codeEscaped})([^<]*?<)`, 'gi'), 
        '$1<mark class="source-highlight">$2</mark>$3')
    }
  }
  
  // 1. 先尝试精确匹配
  const escaped = sourceText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  const exactRegex = new RegExp(`(${escaped})`, 'gi')
  if (exactRegex.test(html)) {
    return html.replace(exactRegex, '<mark class="source-highlight">$1</mark>')
  }
  
  // 2. 尝试匹配前50个字符（处理截断的情况）
  const prefix = sourceText.slice(0, 50).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  const prefixRegex = new RegExp(`(${prefix}[^<]*)`, 'gi')
  if (prefixRegex.test(html)) {
    return html.replace(prefixRegex, '<mark class="source-highlight">$1</mark>')
  }
  
  // 3. 对于表格内容，尝试高亮包含关键词的表格行
  // 提取关键词（去除常见词，取前几个有意义的词）
  const keywords = sourceText
    .replace(/[，。、：；""''（）\[\]【】\n\r]/g, ' ')
    .split(/\s+/)
    .filter(w => w.length >= 2)
    .slice(0, 5)
  
  if (keywords.length > 0) {
    // 高亮包含关键词的 <tr> 或 <td>
    let result = html
    for (const kw of keywords) {
      const kwEscaped = kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
      // 高亮关键词本身
      const kwRegex = new RegExp(`(${kwEscaped})`, 'gi')
      result = result.replace(kwRegex, '<mark class="source-highlight">$1</mark>')
    }
    return result
  }
  
  return html
}
const parseResultSource = computed(() => {
  if (!parseResultContent.value) return ''
  // 源码视图：将 base64 数据替换为简短占位符
  return parseResultContent.value.replace(
    /src="data:[^"]+"/g, 'src="[图片数据已省略]"'
  ).replace(
    /!\[([^\]]*)\]\(data:[^;]+;base64,[A-Za-z0-9+/=]+\)/g,
    '![$1](📷 [图片数据已省略])'
  )
})
// 原件预览（集成在解析结果弹窗中）
const previewContentType = ref('') // 'image' | 'pdf' | 'html' | 'text' | ''
const previewContentUrl = ref('')
const previewContentHtml = ref('')
const previewContentText = ref('')
const parseResultPreviewAvailable = ref(false) // 是否有原件可预览
const parseResultOriginAtt = ref(null)         // 关联的独立附件对象（非 ZIP）
const parseResultOriginZf = ref(null)          // 关联的 zip file entry 对象
// ZIP 内容展示
const showZipContentsDialog = ref(false)
const zipContentsAttName = ref('')
const zipFileList = ref([]) // [{ name, size, ext, parseable, parsing, parsed, parseResult, isHtml }]
const zipInstance = ref(null) // 当前打开的 JSZip 实例
const zipParentAtt = ref(null) // 当前打开的 ZIP 附件对象
// 缓存上传的原始文件对象，用于后续解析
const attachmentFileCache = new Map()
const creatingProject = ref(false)
const newProjectForm = reactive({ title: '', description: '', docxFile: null })
const docxUploadRef = ref(null)
const docxParseProgress = ref(0)
const docxParseStatus = ref('')
const docxParseMessage = ref('')
const parsingProjectId = ref(null)  // 正在解析的项目ID
const newElementForm = reactive({ elementName: '', elementKey: '', dataType: 'text', description: '' })
const newRuleForm = reactive({ 
  ruleName: '', 
  ruleType: 'direct_entity', 
  targetElementKey: '',
  sourceAttachmentId: null,
  locatorType: 'full_text',
  chapterTitle: '',
  reviewCode: '',
  tableSelector: '',
  prompt: ''
})

const filteredProjects = computed(() => {
  if (!projectSearchKeyword.value) return projects.value
  const kw = projectSearchKeyword.value.toLowerCase()
  return projects.value.filter(p => p.title?.toLowerCase().includes(kw))
})


const filledValueCount = computed(() => values.value.filter(v => v.isFilled).length)

// 面板拖拽
function startResizeLeft() {
  isResizing.value = true; resizeType.value = 'left'
  document.addEventListener('mousemove', handleResize)
  document.addEventListener('mouseup', stopResize)
  document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'
}
function startResizeRight() {
  isResizing.value = true; resizeType.value = 'right'
  document.addEventListener('mousemove', handleResize)
  document.addEventListener('mouseup', stopResize)
  document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'
}
function handleResize(e) {
  if (!isResizing.value) return
  if (resizeType.value === 'left') leftPanelWidth.value = Math.max(240, Math.min(500, e.clientX))
  else if (resizeType.value === 'right') rightPanelWidth.value = Math.max(280, Math.min(500, window.innerWidth - e.clientX))
}
function stopResize() {
  isResizing.value = false; resizeType.value = ''
  document.removeEventListener('mousemove', handleResize)
  document.removeEventListener('mouseup', stopResize)
  document.body.style.cursor = ''; document.body.style.userSelect = ''
}

// 项目操作
async function loadProjects() {
  loadingProjects.value = true
  try {
    const data = await projectApi.list({ page: 1, size: 50 })
    projects.value = data?.records || data || []
  } catch (error) {
    console.warn('获取项目列表失败:', error)
    projects.value = []
  } finally { loadingProjects.value = false }
}

async function switchProject(project) {
  if (currentProjectId.value === project.id) { unselectProject(); return }
  currentProjectId.value = project.id
  projectTitle.value = project.title || ''
  leftPanelTab.value = 'projects'
  await loadProjectData(project.id)
}

function unselectProject() {
  currentProjectId.value = null; projectTitle.value = ''
  elements.value = []; values.value = []; attachments.value = []; rules.value = []
  docContent.value = null; docHtml.value = ''
  leftPanelTab.value = 'projects'
}

async function loadProjectData(projectId) {
  loading.value = true
  try {
    const [elemData, valData, attData, ruleData] = await Promise.all([
      elementApi.list(projectId).catch(() => []),
      valueApi.list(projectId).catch(() => []),
      attachmentApi.list(projectId).catch(() => []),
      ruleApi.list(projectId).catch(() => [])
    ])
    elements.value = elemData || []; values.value = valData || []
    attachments.value = attData || []
    // 恢复已持久化的解析状态
    restoreParseStates()
    rules.value = ruleData || []
    // 加载项目模板文档内容
    loadDocContent(projectId)
  } catch (error) { console.error('加载项目数据失败:', error) }
  finally { loading.value = false }
}

// DOCX文件选择处理
function handleDocxFileChange(file) {
  if (file && file.raw) {
    newProjectForm.docxFile = file.raw
  }
}

function handleDocxFileRemove() {
  newProjectForm.docxFile = null
  docxParseProgress.value = 0
  docxParseStatus.value = ''
  docxParseMessage.value = ''
}

function handleCancelNewProject() {
  showNewProjectDialog.value = false
  newProjectForm.title = ''
  newProjectForm.description = ''
  newProjectForm.docxFile = null
  docxParseProgress.value = 0
  docxParseStatus.value = ''
  docxParseMessage.value = ''
  if (docxUploadRef.value) {
    docxUploadRef.value.clearFiles()
  }
}

async function handleCreateProject() {
  if (!newProjectForm.title.trim()) return
  creatingProject.value = true
  
  const hasDocxFile = !!newProjectForm.docxFile
  const docxFile = newProjectForm.docxFile  // 保存文件引用
  
  try {
    // 1. 创建项目
    const project = await projectApi.create({ title: newProjectForm.title.trim(), description: newProjectForm.description })
    
    // 2. 立即关闭弹窗并刷新项目列表
    handleCancelNewProject()
    await loadProjects()
    
    // 3. 如果有DOCX文件，在文档列表中显示解析进度
    if (hasDocxFile && project) {
      parsingProjectId.value = project.id
      docxParseProgress.value = 5
      docxParseMessage.value = '正在上传文档...'
      
      try {
        // 使用智能提取（自动选择同步/异步）
        const result = await extractApi.smartExtract(
          docxFile,
          0,  // attachmentId暂时为0
          false,  // 先不使用LLM，快速测试
          (progress, message) => {
            docxParseProgress.value = 5 + Math.floor(progress * 0.85)
            docxParseMessage.value = message
          }
        )
        
        if (result.success) {
          docxParseProgress.value = 92
          docxParseMessage.value = '正在保存...'
          
          // 保存doc_content到项目
          if (result.doc_content) {
            await projectApi.saveDocContent(project.id, result.doc_content)
          }
          
          // 保存提取的实体
          const entityCount = result.entities?.length || 0
          const llmCount = result.llm_extractions?.length || 0
          console.log(`NER提取: ${entityCount} 个实体`)
          console.log(`LLM提取: ${llmCount} 个内容`)
          
          if (result.entities && result.entities.length > 0) {
            docxParseMessage.value = `正在保存 ${entityCount} 个实体...`
            try {
              // 转换为后端需要的格式
              const entitiesToSave = result.entities.map(e => ({
                name: e.text,
                entityType: e.type,
                value: e.text,
                confidence: e.confidence || 0.9,
                position: e.position ? JSON.stringify(e.position) : null
              }))
              await entityApi.batchCreate(project.id, entitiesToSave)
              console.log(`已保存 ${entityCount} 个实体到数据库`)
            } catch (saveError) {
              console.error('保存实体失败:', saveError)
              // 不阻断流程，继续执行
            }
          }
          
          docxParseProgress.value = 100
          docxParseStatus.value = 'success'
          docxParseMessage.value = `完成！识别 ${entityCount} 个实体`
          
          ElMessage.success(`解析完成，识别 ${entityCount} 个实体`)
          
          // 切换到该项目并刷新数据
          await switchProject(project)
        } else {
          docxParseStatus.value = 'warning'
          docxParseMessage.value = '解析失败'
          ElMessage.warning('文档解析失败')
        }
      } catch (parseError) {
        console.error('DOCX解析失败:', parseError)
        docxParseStatus.value = 'exception'
        docxParseMessage.value = '解析出错'
        ElMessage.warning('文档解析失败: ' + parseError.message)
      }
      
      // 3秒后清除进度显示
      setTimeout(() => {
        if (parsingProjectId.value === project.id) {
          parsingProjectId.value = null
          docxParseProgress.value = 0
          docxParseStatus.value = ''
          docxParseMessage.value = ''
        }
      }, 3000)
    } else {
      ElMessage.success('项目创建成功')
      if (project) await switchProject(project)
    }
    
  } catch (error) { 
    ElMessage.error('创建失败: ' + error.message) 
  }
  finally { creatingProject.value = false }
}

function sendAiMessage() {
  const text = aiInputText.value.trim()
  if (!text) return
  aiMessages.value.push({ role: 'user', content: text })
  aiInputText.value = ''
  // 模拟 AI 回复（后续接入真实 API）
  setTimeout(() => {
    aiMessages.value.push({ role: 'assistant', content: '收到您的消息，AI 助手功能正在开发中，敬请期待...' })
    if (aiMessagesRef.value) aiMessagesRef.value.scrollTop = aiMessagesRef.value.scrollHeight
  }, 500)
  setTimeout(() => {
    if (aiMessagesRef.value) aiMessagesRef.value.scrollTop = aiMessagesRef.value.scrollHeight
  }, 50)
}

function handleFooterCommand(cmd) {
  if (cmd === 'settings') {
    ElMessage.info('系统设置开发中...')
  } else if (cmd === 'logout') {
    ElMessageBox.confirm('确定要退出登录吗？', '退出确认', {
      confirmButtonText: '退出', cancelButtonText: '取消', type: 'warning'
    }).then(async () => {
      try { await import('@/api').then(m => m.authApi.logout()) } catch (e) { /* ignore */ }
      localStorage.removeItem('accessToken')
      localStorage.removeItem('refreshToken')
      localStorage.removeItem('userId')
      localStorage.removeItem('username')
      ElMessage.success('已退出登录')
      router.push('/login')
    }).catch(() => {})
  }
}

async function handleProjectCommand(cmd, project) {
  switch (cmd) {
    case 'copy':
      try { await projectApi.copy(project.id); await loadProjects(); ElMessage.success('项目复制成功') }
      catch (e) { ElMessage.error('复制失败: ' + e.message) }
      break
    case 'archive':
      try { await projectApi.archive(project.id); await loadProjects(); ElMessage.success('项目已归档') }
      catch (e) { ElMessage.error('归档失败: ' + e.message) }
      break
    case 'delete':
      try {
        await ElMessageBox.confirm(`确定要删除项目「${project.title}」吗？`, '删除确认', { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' })
        await projectApi.delete(project.id)
        if (currentProjectId.value === project.id) unselectProject()
        await loadProjects(); ElMessage.success('项目已删除')
      } catch (e) { if (e !== 'cancel') ElMessage.error('删除失败: ' + e.message) }
      break
  }
}

async function handleCopyProject() {
  if (!currentProjectId.value) return
  try {
    const copied = await projectApi.copy(currentProjectId.value)
    await loadProjects(); ElMessage.success('项目复制成功')
    if (copied) await switchProject(copied)
  } catch (e) { ElMessage.error('复制失败: ' + e.message) }
}

function handleSave() { saved.value = true; ElMessage.success('保存成功') }

function handleTitlebarCommand(cmd) {
  if (cmd === 'save') handleSave()
  else if (cmd === 'copy') handleCopyProject()
  else if (cmd === 'export') ElMessage.info('导出功能开发中...')
}

// ==================== 文档预览 + 可编辑 + 要素高亮 ====================

async function loadDocContent(projectId) {
  if (!projectId) return
  docLoading.value = true
  docContent.value = null
  docHtml.value = ''
  try {
    const data = await projectApi.getDocContent(projectId)
    docContent.value = data
    renderDocHtml()
    renderDocHtmlTemplate()
  } catch (e) {
    console.warn('加载文档内容失败:', e)
    docContent.value = null
    docHtml.value = ''
  } finally {
    docLoading.value = false
  }
}

const docPaperStyle = computed(() => {
  const page = docContent.value?.page
  if (!page) return {}
  return {
    maxWidth: `${page.widthMm * 3.78}px`,
    paddingTop: `${page.marginTopMm * 3.78}px`,
    paddingBottom: `${page.marginBottomMm * 3.78}px`,
    paddingLeft: `${page.marginLeftMm * 3.78}px`,
    paddingRight: `${page.marginRightMm * 3.78}px`,
  }
})

// 从值的 elementKey 中提取不含项目前缀的 key
// 例如 "PRJ-2024-001:basicInfo.projectCode" -> "basicInfo.projectCode"
function stripValueKeyPrefix(valueElementKey) {
  const idx = valueElementKey?.indexOf(':')
  return idx >= 0 ? valueElementKey.substring(idx + 1) : valueElementKey
}

// 当前弹出框要素关联的规则列表
const popoverRelatedRules = computed(() => {
  const key = highlightPopover.elementKey
  if (!key) return []
  return rules.value.filter(r => {
    const rk = stripValueKeyPrefix(r.elementKey)
    return rk === key
  })
})

function ruleActionLabel(actionType) {
  const map = {
    quote: '引用',
    summary: 'AI总结',
    ai_extract: 'AI提取',
    table_extract: '表格提取',
    use_entity_value: '人工录入',
  }
  return map[actionType] || actionType
}

function getActionTypeTagType(actionType) {
  const map = {
    quote: '',
    summary: 'success',
    ai_extract: 'warning',
    table_extract: 'danger',
    use_entity_value: 'info',
  }
  return map[actionType] || ''
}

function copyRuleEngineJson() {
  const json = JSON.stringify(ruleEngineAdaptedData.value, null, 2)
  navigator.clipboard.writeText(json).then(() => {
    ElMessage.success('已复制规则引擎数据到剪贴板')
  }).catch(() => {
    ElMessage.error('复制失败')
  })
}

// 从规则中提取来源文本（引用原文 / 规则描述）
function ruleSourceText(rule) {
  // 优先从 actionConfig.sourceText 取（前端引用创建的规则）
  if (rule.actionConfig) {
    try {
      const cfg = typeof rule.actionConfig === 'string' ? JSON.parse(rule.actionConfig) : rule.actionConfig
      if (cfg.sourceText) return cfg.sourceText
      if (cfg.description) return cfg.description
    } catch (_) { /* ignore */ }
  }
  // 其次取 dslContent（mock 规则的取值规则描述）
  if (rule.dslContent) return rule.dslContent
  // 最后取 description
  if (rule.description) return rule.description
  return ''
}

// 构建要素值映射表，分为长文本、短文本、静态文本三类
function buildElementValueMap() {
  const longTexts = []    // paragraph/table 类型的长文本要素
  const shortTexts = []   // text 类型的短文本要素（动态）
  const staticTexts = []  // static 类型的静态要素
  const colors = [
    '#fff3cd', '#cce5ff', '#d4edda', '#f8d7da', '#e2d5f1',
    '#d1ecf1', '#ffeeba', '#c3e6cb', '#f5c6cb', '#d6d8db'
  ]
  let colorIdx = 0
  for (const elem of elements.value) {
    const elemValues = values.value.filter(v => stripValueKeyPrefix(v.elementKey) === elem.elementKey)
    const elemType = elem.elementType || 'text'
    const isStatic = elemType === 'static'
    for (const val of elemValues) {
      const text = val.valueText
      if (!text || text.length < 2) continue
      const entry = {
        text,
        elementKey: elem.elementKey,
        fullElementKey: val.elementKey,
        elementName: elem.elementName,
        valueId: val.valueId,
        elemType,
        isStatic,
        color: isStatic ? '#e8e8e8' : colors[colorIdx % colors.length]
      }
      if (isStatic) {
        staticTexts.push(entry)
      } else if (elemType === 'paragraph' || elemType === 'table') {
        longTexts.push(entry)
      } else {
        shortTexts.push(entry)
      }
    }
    if (!isStatic) colorIdx++
  }
  // 长文本按长度降序
  longTexts.sort((a, b) => b.text.length - a.text.length)
  // 短文本按长度降序
  shortTexts.sort((a, b) => b.text.length - a.text.length)
  // 静态文本按长度降序
  staticTexts.sort((a, b) => b.text.length - a.text.length)
  return { longTexts, shortTexts, staticTexts }
}

// 将文档 blocks 渲染为 HTML 字符串（含要素高亮）
function renderDocHtml() {
  if (!docContent.value?.blocks) { docHtml.value = ''; return }
  const blocks = docContent.value.blocks
  const { longTexts, shortTexts, staticTexts } = highlightEnabled.value ? buildElementValueMap() : { longTexts: [], shortTexts: [], staticTexts: [] }
  // 合并短文本和静态文本用于 runs 级别匹配（动态优先，静态在后）
  const allShortTexts = [...shortTexts, ...staticTexts]
  let highlightCount = 0
  const parts = []

  // 预处理：为每个长文本要素，将其 valueText 按行拆分为句子集合
  // 用于判断某个 block 的文本是否属于某个长文本要素
  const longTextLines = longTexts.map(lt => ({
    ...lt,
    lines: new Set(lt.text.split('\n').map(l => l.trim()).filter(l => l.length > 0))
  }))

  // 收集被长文本高亮覆盖的 block IDs，这些 block 内的短文本不再单独高亮
  const longHighlightedBlockIds = new Set()

  // 第一遍：确定哪些 block 属于长文本要素
  for (const block of blocks) {
    const blockText = getBlockPlainText(block)
    if (!blockText) continue
    for (const lt of longTextLines) {
      if (lt.lines.has(blockText)) {
        longHighlightedBlockIds.add(block.id)
        break
      }
    }
  }

  // 第二遍：渲染，连续属于同一长文本要素的 block 合并到一个边框内
  let currentLongKey = null   // 当前正在收集的长文本要素 key
  let currentLongMatch = null // 当前长文本要素匹配信息
  let longGroupHtml = ''      // 当前长文本分组的 HTML 累积

  function flushLongGroup() {
    if (currentLongKey && longGroupHtml) {
      const borderColor = darkenColor(currentLongMatch.color)
      parts.push(`<div class="elem-highlight-wrap" data-elem-key="${currentLongMatch.elementKey}" data-value-id="${currentLongMatch.valueId || ''}" title="${escapeAttr(currentLongMatch.elementName)}" style="border:2px solid ${borderColor};border-radius:4px;padding:6px 8px;margin:4px 0;cursor:pointer;background:${currentLongMatch.color};">${longGroupHtml}</div>`)
      highlightCount++
    }
    currentLongKey = null
    currentLongMatch = null
    longGroupHtml = ''
  }

  for (const block of blocks) {
    if (block.type === 'table') {
      flushLongGroup()
      const tableMatch = findTableLongTextMatch(block, longTextLines)
      parts.push(renderTableHtml(block, tableMatch))
      if (tableMatch) highlightCount++
    } else {
      const blockText = getBlockPlainText(block)
      const longMatch = findBlockLongTextMatch(blockText, longTextLines)

      if (longMatch) {
        // 如果和当前分组是同一个要素，继续累积
        if (currentLongKey === longMatch.elementKey) {
          longGroupHtml += renderBlockHtml(block, [], null, () => {})
        } else {
          // 不同要素，先 flush 上一组，再开始新组
          flushLongGroup()
          currentLongKey = longMatch.elementKey
          currentLongMatch = longMatch
          longGroupHtml = renderBlockHtml(block, [], null, () => {})
        }
      } else {
        // 非长文本 block，先 flush 再正常渲染
        flushLongGroup()
        parts.push(renderBlockHtml(block, allShortTexts, null, (n) => { highlightCount += n }))
      }
    }
  }
  flushLongGroup() // flush 最后一组

  elementHighlightCount.value = highlightCount
  docHtml.value = parts.join('')
}

// 要素视图：将动态要素值替换为 {{key}} 占位符
function renderDocHtmlTemplate() {
  if (!docContent.value?.blocks) { docTemplateHtml.value = ''; return }
  const blocks = docContent.value.blocks
  const { longTexts, shortTexts } = buildElementValueMap()
  // 合并所有动态要素（不含静态）
  const allDynamic = [...shortTexts]

  const longTextLines = longTexts.map(lt => ({
    ...lt,
    lines: new Set(lt.text.split('\n').map(l => l.trim()).filter(l => l.length > 0))
  }))

  const parts = []
  let currentLongKey = null
  let currentLongMatch = null
  let longGroupHtml = ''

  function flushLongGroup() {
    if (currentLongKey && longGroupHtml) {
      const tag = `<span class="elem-tpl-tag" title="${escapeAttr(currentLongMatch.elementName)}">{{${currentLongMatch.elementKey}}}</span>`
      parts.push(`<div class="elem-tpl-block">${tag}</div>`)
    }
    currentLongKey = null
    currentLongMatch = null
    longGroupHtml = ''
  }

  for (const block of blocks) {
    if (block.type === 'table') {
      flushLongGroup()
      const tableMatch = findTableLongTextMatch(block, longTextLines)
      if (tableMatch) {
        const tag = `<span class="elem-tpl-tag" title="${escapeAttr(tableMatch.elementName)}">{{${tableMatch.elementKey}}}</span>`
        parts.push(`<div class="elem-tpl-block">${tag}</div>`)
      } else {
        parts.push(renderTableHtml(block, null))
      }
    } else {
      const blockText = getBlockPlainText(block)
      const longMatch = findBlockLongTextMatch(blockText, longTextLines)

      if (longMatch) {
        if (currentLongKey === longMatch.elementKey) {
          longGroupHtml += 'x' // just accumulate
        } else {
          flushLongGroup()
          currentLongKey = longMatch.elementKey
          currentLongMatch = longMatch
          longGroupHtml = 'x'
        }
      } else {
        flushLongGroup()
        parts.push(renderBlockHtmlTemplate(block, allDynamic))
      }
    }
  }
  flushLongGroup()
  docTemplateHtml.value = parts.join('')
}

// 渲染单个 block，将匹配的动态短文本替换为 {{key}} 标签
function renderBlockHtmlTemplate(block, shortMap) {
  const tag = getBlockTag(block.type)
  const cls = `doc-block doc-${block.type}`
  const styleStr = buildStyleStr(block.style)
  const styleAttr = styleStr ? ` style="${styleStr}"` : ''

  let inner = ''
  // 图片
  if (block.images?.length > 0) {
    for (const img of block.images) {
      const imgStyle = []
      if (img.widthInch) imgStyle.push(`width:${img.widthInch}in`)
      if (img.heightInch) imgStyle.push(`height:${img.heightInch}in`)
      imgStyle.push('max-width:100%', 'display:block', 'margin:8px auto')
      inner += `<img src="${img.src}" style="${imgStyle.join(';')}" class="doc-inline-image" contenteditable="false" />`
    }
  }
  // Runs - 替换动态要素值为 {{key}}
  if (block.runs) {
    if (shortMap.length === 0) {
      for (const run of block.runs) {
        const text = escapeHtml(run.text)
        const rs = buildRunStyleStr(run)
        inner += rs ? `<span style="${rs}">${text}</span>` : text
      }
    } else {
      inner += templateReplaceRuns(block.runs, shortMap)
    }
  }

  if (!inner) inner = '&nbsp;'
  return `<${tag} class="${cls}"${styleAttr} data-block-id="${block.id}">${inner}</${tag}>`
}

// 在 runs 纯文本中查找动态要素值并替换为 {{key}} 标签
function templateReplaceRuns(runs, shortMap) {
  // 构建纯文本
  let plainText = ''
  const charToRun = []
  for (let ri = 0; ri < runs.length; ri++) {
    const t = runs[ri].text || ''
    for (let ci = 0; ci < t.length; ci++) {
      charToRun.push({ runIdx: ri, offsetInRun: ci })
      plainText += t[ci]
    }
  }

  // 查找匹配
  const matches = []
  for (const em of shortMap) {
    const val = em.text
    if (!val || val.length < 2) continue
    let pos = 0
    while (true) {
      const idx = plainText.indexOf(val, pos)
      if (idx < 0) break
      matches.push({ start: idx, end: idx + val.length, em })
      pos = idx + val.length
    }
  }

  matches.sort((a, b) => a.start - b.start || b.end - a.end)
  const filtered = []
  let lastEnd = -1
  for (const m of matches) {
    if (m.start >= lastEnd) {
      filtered.push(m)
      lastEnd = m.end
    }
  }

  if (filtered.length === 0) {
    let html = ''
    for (const run of runs) {
      const text = escapeHtml(run.text)
      const rs = buildRunStyleStr(run)
      html += rs ? `<span style="${rs}">${text}</span>` : text
    }
    return html
  }

  // 切分为普通段 + 替换段
  const segments = []
  let cursor = 0
  for (const m of filtered) {
    if (m.start > cursor) segments.push({ start: cursor, end: m.start, em: null })
    segments.push({ start: m.start, end: m.end, em: m.em })
    cursor = m.end
  }
  if (cursor < plainText.length) segments.push({ start: cursor, end: plainText.length, em: null })

  let html = ''
  for (const seg of segments) {
    if (seg.em) {
      // 替换为 {{key}} 标签
      html += `<span class="elem-tpl-tag" title="${escapeAttr(seg.em.elementName)}">{{${seg.em.elementKey}}}</span>`
    } else {
      // 普通文本保留 run 样式
      const segChars = charToRun.slice(seg.start, seg.end)
      const groups = []
      let curGroup = null
      for (const ch of segChars) {
        if (!curGroup || curGroup.runIdx !== ch.runIdx) {
          curGroup = { runIdx: ch.runIdx, startOffset: ch.offsetInRun, endOffset: ch.offsetInRun + 1 }
          groups.push(curGroup)
        } else {
          curGroup.endOffset = ch.offsetInRun + 1
        }
      }
      for (const g of groups) {
        const run = runs[g.runIdx]
        const slice = escapeHtml(run.text.substring(g.startOffset, g.endOffset))
        const rs = buildRunStyleStr(run)
        html += rs ? `<span style="${rs}">${slice}</span>` : slice
      }
    }
  }
  return html
}

// 切换视图模式时触发模板渲染
watch(viewMode, (mode) => {
  if (mode === 'elements' && docContent.value) {
    renderDocHtmlTemplate()
  }
})

// 替换文档 blocks 中的旧值文本为新值
function replaceTextInBlocks(blocks, oldText, newText) {
  for (const block of blocks) {
    if (block.type === 'table' && block.rows) {
      // 表格：遍历每行每个单元格
      for (const row of block.rows) {
        for (const cell of row) {
          if (cell.blocks) replaceTextInBlocks(cell.blocks, oldText, newText)
        }
      }
      continue
    }
    if (!block.runs || block.runs.length === 0) continue
    // 将 runs 拼接后检查是否包含旧值
    const fullText = block.runs.map(r => r.text).join('')
    if (!fullText.includes(oldText)) continue
    // 简单情况：旧值完整存在于单个 run 中
    let replaced = false
    for (const run of block.runs) {
      if (run.text && run.text.includes(oldText)) {
        run.text = run.text.replace(oldText, newText)
        replaced = true
        break
      }
    }
    if (replaced) continue
    // 复杂情况：旧值跨越多个 runs —— 重建 runs
    const newFullText = fullText.replace(oldText, newText)
    // 保留第一个 run 的样式，将所有文本合并到第一个 run
    if (block.runs.length > 0) {
      block.runs[0].text = newFullText
      block.runs.length = 1
    }
  }
}

// 获取 block 的纯文本
function getBlockPlainText(block) {
  if (!block.runs) return ''
  return block.runs.map(r => r.text).join('').trim()
}

// 查找 block 文本是否匹配某个长文本要素
function findBlockLongTextMatch(blockText, longTextLines) {
  if (!blockText) return null
  for (const lt of longTextLines) {
    if (lt.lines.has(blockText)) return lt
  }
  return null
}

// 查找表格是否匹配某个长文本要素（通过表格第一行文本匹配）
function findTableLongTextMatch(block, longTextLines) {
  if (!block.table?.data?.length) return null
  const firstRowText = block.table.data[0].map(c => c.text).join(' | ')
  for (const lt of longTextLines) {
    if (lt.text.includes(firstRowText)) return lt
  }
  return null
}

function renderBlockHtml(block, shortMap, longMatch, countFn) {
  const tag = getBlockTag(block.type)
  const cls = `doc-block doc-${block.type}`
  const styleStr = buildStyleStr(block.style)
  const styleAttr = styleStr ? ` style="${styleStr}"` : ''

  const isToc = block.type?.startsWith('toc')

  let inner = ''
  // 图片
  if (block.images?.length > 0) {
    for (const img of block.images) {
      const imgStyle = []
      if (img.widthInch) imgStyle.push(`width:${img.widthInch}in`)
      if (img.heightInch) imgStyle.push(`height:${img.heightInch}in`)
      imgStyle.push('max-width:100%', 'display:block', 'margin:8px auto')
      inner += `<img src="${img.src}" style="${imgStyle.join(';')}" class="doc-inline-image" contenteditable="false" />`
    }
  }
  // Runs
  if (block.runs) {
    if (isToc) {
      // 目录项：忽略 run 内联样式，由 CSS 统一控制外观
      for (const run of block.runs) {
        inner += escapeHtml(run.text)
      }
    } else if (longMatch || shortMap.length === 0) {
      // 长文本高亮或无需短文本高亮，直接渲染
      for (const run of block.runs) {
        const text = escapeHtml(run.text)
        const rs = buildRunStyleStr(run)
        inner += rs ? `<span style="${rs}">${text}</span>` : text
      }
      if (longMatch) countFn(1)
    } else {
      // 短文本高亮：基于纯文本位置匹配，支持跨 run 拆分的文本
      const result = highlightRunsWithElements(block.runs, shortMap)
      inner += result.html
      if (result.count > 0) countFn(result.count)
    }
  }

  if (!inner) inner = '&nbsp;'
  return `<${tag} class="${cls}"${styleAttr} data-block-id="${block.id}">${inner}</${tag}>`
}

function renderTableHtml(block, longMatch) {
  const t = block.table
  if (!t?.data) return ''
  let html = `<table class="doc-table" data-block-id="${block.id}">`
  for (let ri = 0; ri < t.data.length; ri++) {
    html += '<tr>'
    for (const cell of t.data[ri]) {
      const cs = cell.colspan > 1 ? ` colspan="${cell.colspan}"` : ''
      html += `<td class="doc-table-cell"${cs}>${escapeHtml(cell.text)}</td>`
    }
    html += '</tr>'
  }
  html += '</table>'
  if (longMatch) {
    const borderColor = darkenColor(longMatch.color)
    html = `<div class="elem-highlight-wrap" data-elem-key="${longMatch.elementKey}" data-value-id="${longMatch.valueId || ''}" title="${escapeAttr(longMatch.elementName)}" style="border:2px solid ${borderColor};border-radius:8px;padding:8px;margin:12px 0;cursor:pointer;background:${longMatch.color};">${html}</div>`
  }
  return html
}

function getBlockTag(type) {
  if (type === 'heading1') return 'h1'
  if (type === 'heading2') return 'h2'
  if (type === 'heading3') return 'h3'
  if (type?.startsWith('toc')) return 'div'
  return 'p'
}

function buildStyleStr(style) {
  if (!style) return ''
  const parts = []
  if (style.alignment) {
    const map = { left: 'left', center: 'center', right: 'right', justify: 'justify', both: 'justify' }
    parts.push(`text-align:${map[style.alignment] || style.alignment}`)
  }
  if (style.indentLeft) parts.push(`padding-left:${style.indentLeft / 914400}in`)
  if (style.indentRight) parts.push(`padding-right:${style.indentRight / 914400}in`)
  if (style.indentFirstLine) parts.push(`text-indent:${style.indentFirstLine / 914400}in`)
  if (style.indentHanging) parts.push(`text-indent:-${style.indentHanging / 914400}in`)
  if (style.spacingBefore) parts.push(`margin-top:${style.spacingBefore / 914400}in`)
  if (style.spacingAfter) parts.push(`margin-bottom:${style.spacingAfter / 914400}in`)
  if (style.lineSpacing && style.lineSpacing >= 1 && style.lineSpacing <= 5) parts.push(`line-height:${style.lineSpacing}`)
  return parts.join(';')
}

function buildRunStyleStr(run) {
  const parts = []
  if (run.fontFamily) parts.push(`font-family:${run.fontFamily}`)
  if (run.fontSize) parts.push(`font-size:${run.fontSize}pt`)
  if (run.bold) parts.push('font-weight:bold')
  if (run.italic) parts.push('font-style:italic')
  if (run.color) parts.push(`color:${run.color.startsWith('#') ? run.color : '#' + run.color}`)
  if (run.underline) parts.push('text-decoration:underline')
  if (run.strikeThrough) parts.push('text-decoration:line-through')
  return parts.join(';')
}

function escapeHtml(text) {
  if (!text) return ''
  return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
}

function escapeAttr(text) {
  if (!text) return ''
  return text.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;')
}

// 基于纯文本位置的短文本高亮，支持跨 run 拆分的文本匹配
function highlightRunsWithElements(runs, shortMap) {
  // 1. 构建纯文本和每个字符到 run 的映射
  let plainText = ''
  const charToRun = [] // charToRun[i] = { runIdx, offsetInRun }
  for (let ri = 0; ri < runs.length; ri++) {
    const t = runs[ri].text || ''
    for (let ci = 0; ci < t.length; ci++) {
      charToRun.push({ runIdx: ri, offsetInRun: ci })
      plainText += t[ci]
    }
  }

  // 2. 在纯文本中查找所有要素值的匹配位置
  const matches = [] // { start, end, em }
  for (const em of shortMap) {
    const val = em.text
    if (!val || val.length < 2) continue
    let pos = 0
    while (true) {
      const idx = plainText.indexOf(val, pos)
      if (idx < 0) break
      matches.push({ start: idx, end: idx + val.length, em })
      pos = idx + val.length
    }
  }

  // 3. 去重：按 start 排序，移除被更长匹配覆盖的（已按长度降序排列的 shortMap 保证优先）
  matches.sort((a, b) => a.start - b.start || b.end - a.end)
  const filtered = []
  let lastEnd = -1
  for (const m of matches) {
    if (m.start >= lastEnd) {
      filtered.push(m)
      lastEnd = m.end
    }
  }

  if (filtered.length === 0) {
    // 无匹配，直接渲染
    let html = ''
    for (const run of runs) {
      const text = escapeHtml(run.text)
      const rs = buildRunStyleStr(run)
      html += rs ? `<span style="${rs}">${text}</span>` : text
    }
    return { html, count: 0 }
  }

  // 4. 将纯文本按匹配区间切分为：普通段 + 高亮段
  const segments = [] // { start, end, em: null|object }
  let cursor = 0
  for (const m of filtered) {
    if (m.start > cursor) {
      segments.push({ start: cursor, end: m.start, em: null })
    }
    segments.push({ start: m.start, end: m.end, em: m.em })
    cursor = m.end
  }
  if (cursor < plainText.length) {
    segments.push({ start: cursor, end: plainText.length, em: null })
  }

  // 5. 对每个 segment，按 run 边界拆分并生成 HTML
  let html = ''
  for (const seg of segments) {
    const segChars = charToRun.slice(seg.start, seg.end)
    // 按 runIdx 分组
    const groups = []
    let curGroup = null
    for (const ch of segChars) {
      if (!curGroup || curGroup.runIdx !== ch.runIdx) {
        curGroup = { runIdx: ch.runIdx, startOffset: ch.offsetInRun, endOffset: ch.offsetInRun + 1 }
        groups.push(curGroup)
      } else {
        curGroup.endOffset = ch.offsetInRun + 1
      }
    }

    if (seg.em) {
      // 高亮段：静态要素用虚线淡色边框，动态要素用实线彩色边框
      const em = seg.em
      const isStatic = em.isStatic
      const borderStyle = isStatic
        ? 'border:1px dashed #ccc;border-radius:4px;padding:2px 6px;cursor:pointer;opacity:0.7;background:#f5f5f5;'
        : `border:1.5px solid ${darkenColor(em.color)};border-radius:4px;padding:2px 6px;cursor:pointer;background:${em.color};color:${darkenColor(em.color)};`
      const hlClass = isStatic ? 'elem-highlight elem-highlight-static' : 'elem-highlight'
      html += `<span class="${hlClass}" data-elem-key="${em.elementKey}" data-value-id="${em.valueId || ''}" style="${borderStyle}" title="${escapeAttr(em.elementName)}">`
      for (const g of groups) {
        const run = runs[g.runIdx]
        const slice = escapeHtml(run.text.substring(g.startOffset, g.endOffset))
        const rs = buildRunStyleStr(run)
        html += rs ? `<span style="${rs}">${slice}</span>` : slice
      }
      html += '</span>'
    } else {
      // 普通段：保留 run 样式
      for (const g of groups) {
        const run = runs[g.runIdx]
        const slice = escapeHtml(run.text.substring(g.startOffset, g.endOffset))
        const rs = buildRunStyleStr(run)
        html += rs ? `<span style="${rs}">${slice}</span>` : slice
      }
    }
  }

  return { html, count: filtered.length }
}

function darkenColor(hex) {
  // 简单加深颜色用于下边框
  const map = {
    '#fff3cd': '#e0a800', '#cce5ff': '#3d8bfd', '#d4edda': '#28a745',
    '#f8d7da': '#dc3545', '#e2d5f1': '#6f42c1', '#d1ecf1': '#17a2b8',
    '#ffeeba': '#d39e00', '#c3e6cb': '#1e7e34', '#f5c6cb': '#c82333',
    '#d6d8db': '#6c757d'
  }
  return map[hex] || '#999'
}

// 文档编辑事件
function onDocInput() {
  saved.value = false
}

// 点击文档中的高亮要素（mousedown 比 click 更可靠，在 contenteditable 容器内 e.target 更准确）
function onDocClick(e) {
  const target = e.target.closest('.elem-highlight') || e.target.closest('.elem-highlight-wrap') || e.target.closest('.elem-highlight-table')
  if (!target) {
    highlightPopover.visible = false
    return
  }
  const elemKey = target.dataset.elemKey
  const valueId = target.dataset.valueId
  const elem = elements.value.find(el => el.elementKey === elemKey)
  const val = values.value.find(v => String(v.valueId) === String(valueId)) ||
              values.value.find(v => stripValueKeyPrefix(v.elementKey) === elemKey)

  if (!elem || elem.elementType === 'static') return
  e.preventDefault()

  const rect = target.getBoundingClientRect()
  const scrollEl = editorRef.value
  const scrollRect = scrollEl?.getBoundingClientRect() || { top: 0, left: 0 }

  highlightPopover.elementKey = elemKey
  highlightPopover.fullElementKey = val?.elementKey || ''
  highlightPopover.elementName = elem.elementName
  highlightPopover.currentValue = val?.valueText || ''
  highlightPopover.originalValue = ''
  highlightPopover.valueId = val?.valueId || null
  highlightPopover.x = rect.left - scrollRect.left + scrollEl.scrollLeft
  highlightPopover.y = rect.bottom - scrollRect.top + scrollEl.scrollTop + 4
  highlightPopover.visible = true
}

async function savePopoverValue() {
  if (!highlightPopover.elementKey || !currentProjectId.value) {
    ElMessage.warning('无法保存：未找到对应的值记录')
    return
  }
  try {
    // 查找本地 value 记录，获取完整的 prefixed elementKey
    const val = values.value.find(v => stripValueKeyPrefix(v.elementKey) === highlightPopover.elementKey)
    const apiKey = highlightPopover.fullElementKey || val?.elementKey || highlightPopover.elementKey
    const oldValue = val?.valueText || ''
    const newValue = highlightPopover.currentValue
    // API: PUT /projects/{projectId}/values/{elementKey} with { valueText }
    await valueApi.update(currentProjectId.value, apiKey, { valueText: newValue })
    // 更新本地数据
    if (val) {
      val.valueText = newValue
      val.isFilled = !!newValue
    }
    // 替换文档 blocks 中的旧值文本为新值
    if (oldValue && newValue && oldValue !== newValue && docContent.value?.blocks) {
      replaceTextInBlocks(docContent.value.blocks, oldValue, newValue)
    }
    highlightPopover.visible = false
    renderDocHtml()
    ElMessage.success('要素值已更新')
  } catch (e) {
    ElMessage.error('保存失败: ' + (e.message || e))
  }
}

// ==================== 附件引用系统 ====================

// 在解析结果中选中文本时触发
function onParseResultMouseUp(e) {
  const sel = window.getSelection()
  const text = sel?.toString()?.trim()
  if (!text || text.length < 2) {
    citationToolbar.visible = false
    return
  }
  citationToolbar.selectedText = text
  // 定位浮动工具栏到选区末端
  const range = sel.getRangeAt(0)
  const rect = range.getBoundingClientRect()
  const dialogEl = e.currentTarget.closest('.el-dialog')
  const dialogRect = dialogEl?.getBoundingClientRect() || { top: 0, left: 0 }
  citationToolbar.x = rect.left - dialogRect.left + rect.width / 2 - 120
  citationToolbar.y = rect.bottom - dialogRect.top + 8
  // 引用模式下已锁定要素，直接进入选操作步骤
  if (referenceMode.value) {
    citationToolbar.step = 'select_action'
  } else {
    citationToolbar.step = 'select_action'
  }
  citationToolbar.visible = true
}

// 选择引用方式（直接引用/AI总结/表格提取）
function setCitationAction(actionType) {
  citationToolbar.actionType = actionType
  if (referenceMode.value) {
    // 已锁定要素，直接确认创建规则
    const elem = elements.value.find(el => el.elementKey === referenceModeElementKey.value)
    if (elem) {
      confirmCitation(elem)
    } else {
      ElMessage.warning('目标要素不存在')
    }
  } else {
    // 未锁定要素，进入选择要素步骤
    citationToolbar.step = 'select_element'
  }
}

// 确认引用：创建规则
async function confirmCitation(elem) {
  const selectedText = citationToolbar.selectedText
  const actionType = citationToolbar.actionType
  if (!selectedText || !actionType || !elem) return

  const projectId = currentProjectId.value
  if (!projectId) { ElMessage.warning('未选择项目'); return }

  try {
    const actionConfig = JSON.stringify({
      sourceText: selectedText,
      attachmentName: referenceModeAttName.value || parseResultAttName.value || '',
    })
    // 查找附件节点ID（如果有的话）
    const attId = referenceModeAttId.value || null
    const attName = referenceModeAttName.value || parseResultAttName.value || ''
    const inputs = attId ? [{ 
      sourceNodeId: attId, 
      inputKey: 'attachment', 
      inputType: 'ATTACHMENT', 
      inputName: attName,
      sourceName: attName,
      sourceText: selectedText  // 传入来源段落文本，用于溯源定位
    }] : []

    const ruleData = {
      elementKey: elem.elementKey,
      ruleName: `${actionType === 'quote' ? '引用' : actionType === 'summary' ? 'AI总结' : '表格提取'} - ${elem.elementName}`,
      ruleType: 'attachment_reference',
      actionType: actionType,
      actionConfig: actionConfig,
      description: `从附件「${referenceModeAttName.value || parseResultAttName.value}」${actionType === 'quote' ? '直接引用' : actionType === 'summary' ? 'AI总结' : '表格提取'}`,
      inputs: inputs
    }

    await ruleApi.create(projectId, ruleData)

    // 如果是直接引用，同时更新要素值
    if (actionType === 'quote') {
      const fullKey = values.value.find(v => stripValueKeyPrefix(v.elementKey) === elem.elementKey)?.elementKey || elem.elementKey
      await valueApi.update(projectId, fullKey, { valueText: selectedText, fillSource: 'rule' })
      const val = values.value.find(v => stripValueKeyPrefix(v.elementKey) === elem.elementKey)
      if (val) {
        val.valueText = selectedText
        val.isFilled = true
        val.fillSource = 'rule'
      }
      renderDocHtml()
    }

    // 刷新规则列表
    rules.value = await ruleApi.list(projectId)

    citationToolbar.visible = false
    citationToolbar.selectedText = ''
    citationToolbar.actionType = ''
    window.getSelection()?.removeAllRanges()

    if (referenceMode.value) exitReferenceMode()

    ElMessage.success(
      actionType === 'quote'
        ? `已引用到「${elem.elementName}」并更新值`
        : `已创建${actionType === 'summary' ? 'AI总结' : '表格提取'}规则 → ${elem.elementName}`
    )
  } catch (e) {
    ElMessage.error('创建引用规则失败: ' + e.message)
  }
}

// 从要素弹出框进入引用模式 → 打开附件选择
function enterReferenceModeFromPopover() {
  const elemKey = highlightPopover.elementKey
  const elemName = highlightPopover.elementName
  if (!elemKey) return
  highlightPopover.visible = false

  // 找已解析的附件
  const parsedAtts = attachments.value.filter(att => {
    const state = parseStates[att.id]
    return state?.status === 'completed' && state.markdown
  })

  if (parsedAtts.length === 0) {
    ElMessage.warning('没有已解析的附件，请先解析附件')
    return
  }

  // 如果只有一个已解析附件，直接打开
  if (parsedAtts.length === 1) {
    openAttachmentInReferenceMode(parsedAtts[0], elemKey, elemName)
    return
  }

  // 多个附件，弹出选择弹窗
  refAttSelectList.value = parsedAtts
  refAttSelectElemKey.value = elemKey
  refAttSelectElemName.value = elemName
  showRefAttSelectDialog.value = true
}

function onRefAttSelected(att) {
  showRefAttSelectDialog.value = false
  openAttachmentInReferenceMode(att, refAttSelectElemKey.value, refAttSelectElemName.value)
}

function openAttachmentInReferenceMode(att, elemKey, elemName) {
  referenceMode.value = true
  referenceModeElementKey.value = elemKey
  referenceModeElementName.value = elemName
  referenceModeAttId.value = typeof att.id === 'number' ? att.id : null
  referenceModeAttName.value = att.displayName

  const state = parseStates[att.id]
  parseResultAttName.value = att.displayName
  parseResultContent.value = state.markdown
  parseResultIsHtml.value = !!state.isHtml
  parseResultPreviewAvailable.value = false
  parseResultOriginAtt.value = att
  parseResultOriginZf.value = null
  previewContentType.value = ''
  parseResultViewMode.value = 'rendered'
  showParseResultDialog.value = true
}

function exitReferenceMode() {
  referenceMode.value = false
  referenceModeElementKey.value = ''
  referenceModeElementName.value = ''
  referenceModeAttId.value = null
  referenceModeAttName.value = ''
  citationToolbar.visible = false
}

// 要素/值
function getElementValues(elementKey) { return values.value.filter(v => stripValueKeyPrefix(v.elementKey) === elementKey) }
function hasFilledValue(elementKey) { return values.value.some(v => stripValueKeyPrefix(v.elementKey) === elementKey && v.isFilled) }
function onValueChange(val) { saved.value = false; val.isModified = true }

async function handleAddElement() {
  if (!newElementForm.elementName || !newElementForm.elementKey) return
  try {
    const elem = await elementApi.add(currentProjectId.value, { ...newElementForm })
    elements.value.push(elem); showAddElementDialog.value = false
    Object.assign(newElementForm, { elementName: '', elementKey: '', dataType: 'text', description: '' })
    ElMessage.success('要素添加成功')
  } catch (e) { ElMessage.error('添加失败: ' + e.message) }
}

// 附件
async function handleAttachmentUpload(file) {
  if (!currentProjectId.value) return
  try {
    const att = await attachmentApi.upload(currentProjectId.value, file.raw, file.name)
    attachments.value.push(att)
    // 缓存原始文件用于后续解析
    if (att?.id) attachmentFileCache.set(att.id, file.raw)
    ElMessage.success('附件上传成功')
  } catch (e) { ElMessage.error('上传失败: ' + e.message) }
}
function selectAttachment(att) { selectedAttachment.value = att }
async function removeAttachment(att) {
  try {
    await ElMessageBox.confirm(`确定删除附件「${att.displayName}」？`, '删除确认', { type: 'warning' })
    await attachmentApi.delete(att.id)
    attachments.value = attachments.value.filter(a => a.id !== att.id); ElMessage.success('附件已删除')
  } catch (e) { if (e !== 'cancel') ElMessage.error('删除失败') }
}
function getFileExt(att) {
  const t = att.fileType || ''
  if (t) return t.toLowerCase()
  const name = att.displayName || att.fileName || ''
  const ext = name.split('.').pop()?.toLowerCase()
  return ext || ''
}

function getAttachmentFetchUrl(att) {
  if (att?.fileUrl) return att.fileUrl
  if (att?.fileKey) return `/api/v1/files/${encodeURIComponent(att.fileKey)}`
  if (att?.filePath && /^https?:\/\//i.test(att.filePath)) return att.filePath
  return ''
}

async function loadAttachmentFile(att, fallbackName = 'file') {
  const cached = attachmentFileCache.get(att.id)
  if (cached) return cached

  const url = getAttachmentFetchUrl(att)
  if (!url) return null

  try {
    const token = localStorage.getItem('accessToken')
    const headers = token ? { Authorization: `Bearer ${token}` } : {}
    const resp = await fetch(url, { headers })
    if (!resp.ok) throw new Error(`下载失败(${resp.status})`)
    const blob = await resp.blob()
    // 优先使用带扩展名的fileName，否则用displayName+扩展名
    let fileName = att.fileName || fallbackName
    if (!fileName && att.displayName && att.fileType) {
      fileName = `${att.displayName}.${att.fileType}`
    } else if (!fileName && att.displayName) {
      fileName = att.displayName
    }
    const file = new File([blob], fileName, {
      type: blob.type || 'application/octet-stream'
    })
    attachmentFileCache.set(att.id, file)
    return file
  } catch (e) {
    console.warn('获取附件文件失败:', e)
    return null
  }
}

function getFileTypeClass(att) {
  const ext = getFileExt(att)
  if (ext === 'pdf') return 'type-pdf'
  if (ext === 'doc' || ext === 'docx') return 'type-word'
  if (ext === 'xls' || ext === 'xlsx') return 'type-excel'
  if (ext === 'png' || ext === 'jpg' || ext === 'jpeg') return 'type-image'
  if (ext === 'zip' || ext === 'rar' || ext === '7z') return 'type-archive'
  return 'type-other'
}
function getFileTypeLabel(att) {
  const ext = getFileExt(att)
  if (ext === 'pdf') return 'PDF'
  if (ext === 'doc' || ext === 'docx') return 'W'
  if (ext === 'xls' || ext === 'xlsx') return 'X'
  if (ext === 'png' || ext === 'jpg' || ext === 'jpeg') return '🖼'
  if (ext === 'zip' || ext === 'rar' || ext === '7z') return 'ZIP'
  return '📄'
}
function getFileTypeTag(att) {
  const ext = getFileExt(att)
  if (ext === 'pdf') return 'PDF'
  if (ext === 'doc' || ext === 'docx') return 'docx'
  if (ext === 'xls' || ext === 'xlsx') return 'xlsx'
  if (ext === 'png') return 'png'
  if (ext === 'jpg' || ext === 'jpeg') return 'jpg'
  if (ext === 'zip') return 'zip'
  return ext || '文件'
}
function getZipEntryTypeClass(zf) {
  const ext = zf.ext
  if (ext === 'pdf') return 'type-pdf'
  if (ext === 'doc' || ext === 'docx') return 'type-word'
  if (ext === 'xls' || ext === 'xlsx') return 'type-excel'
  if (ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'gif') return 'type-image'
  return 'type-other'
}
function getZipEntryTypeLabel(zf) {
  const ext = zf.ext
  if (ext === 'pdf') return 'PDF'
  if (ext === 'doc' || ext === 'docx') return 'W'
  if (ext === 'xls' || ext === 'xlsx') return 'X'
  if (['png', 'jpg', 'jpeg', 'gif'].includes(ext)) return '🖼'
  return '📄'
}
async function parseAllZipEntries() {
  const pending = zipFileList.value.filter(f => f.parseable && !f.parsed && !f.parsing)
  if (pending.length === 0) return
  ElMessage.info(`开始解析 ${pending.length} 个文件...`)
  for (const zf of pending) {
    await parseZipEntry(zf)
  }
}
function formatFileSize(bytes) {
  if (!bytes) return ''
  if (bytes < 1024) return bytes + 'B'
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(0) + 'KB'
  return (bytes / (1024 * 1024)).toFixed(1) + 'MB'
}
async function handleAttachmentAction(cmd, att) {
  switch (cmd) {
    case 'preview':
      selectAttachment(att)
      ElMessage.info('预览功能开发中')
      break
    case 'parse':
      await handleParseAttachment(att)
      break
    case 'view_result':
      viewParseResult(att)
      break
    case 'apply':
      ElMessage.info('应用要素功能开发中')
      break
    case 'download':
      ElMessage.info('下载功能开发中')
      break
    case 'delete':
      await removeAttachment(att)
      break
  }
}

function viewParseResult(att) {
  const state = parseStates[att.id]
  if (!state || state.status !== 'completed' || !state.markdown) {
    ElMessage.warning('该附件尚未解析或解析结果为空')
    return
  }
  parseResultAttName.value = att.displayName
  parseResultContent.value = state.markdown
  parseResultIsHtml.value = !!state.isHtml
  // 独立附件也支持原件预览（从缓存或 fileUrl 获取）
  const ext = getFileExt(att)
  parseResultPreviewAvailable.value = ['pdf', 'png', 'jpg', 'jpeg', 'gif', 'docx', 'doc'].includes(ext)
  parseResultOriginAtt.value = att
  parseResultOriginZf.value = null
  previewContentType.value = ''
  parseResultViewMode.value = 'rendered'
  showParseResultDialog.value = true
  
  // 如果有高亮文本，延迟滚动到高亮位置
  if (highlightSourceText.value) {
    setTimeout(scrollToHighlight, 300)
  }
}

function scrollToHighlight(retries = 3) {
  console.log('[scrollToHighlight] 尝试滚动定位, retries:', retries)
  // 查找高亮元素
  const highlight = document.querySelector('.parse-result-rendered mark.source-highlight')
  console.log('[scrollToHighlight] 找到高亮元素:', highlight)
  if (highlight) {
    highlight.scrollIntoView({ behavior: 'smooth', block: 'center' })
    // 添加闪烁效果
    highlight.classList.add('highlight-flash')
    setTimeout(() => highlight.classList.remove('highlight-flash'), 2000)
    console.log('[scrollToHighlight] 滚动完成')
  } else if (retries > 0) {
    // 如果没找到，可能是 DOM 还没渲染完，重试
    console.log('[scrollToHighlight] 未找到元素，重试...')
    setTimeout(() => scrollToHighlight(retries - 1), 200)
  } else {
    console.log('[scrollToHighlight] 重试次数用尽，未找到高亮元素')
  }
}

function copyParseResult() {
  if (!parseResultContent.value) return
  navigator.clipboard.writeText(parseResultContent.value).then(() => {
    ElMessage.success('已复制到剪贴板')
  }).catch(() => {
    ElMessage.error('复制失败')
  })
}

function getParseState(attId) {
  if (!parseStates[attId]) {
    parseStates[attId] = { status: 'idle', progress: '', markdown: '' }
  }
  return parseStates[attId]
}

function saveParseState(attId) {
  try {
    const saved = JSON.parse(localStorage.getItem('parseStates') || '{}')
    const state = parseStates[attId]
    if (state?.status === 'completed' && state.markdown) {
      saved[attId] = { status: 'completed', markdown: state.markdown, isHtml: !!state.isHtml }
    }
    try {
      localStorage.setItem('parseStates', JSON.stringify(saved))
    } catch (quotaErr) {
      // localStorage 空间不足（base64 图片太大），保存不含图片的版本
      console.warn('localStorage 空间不足，保存不含图片的精简版本')
      if (state?.markdown) {
        const lite = state.markdown
          .replace(/data:[^;]+;base64,[A-Za-z0-9+/=]+/g, '')
          .replace(/src="data:[^"]+"/g, 'src=""')
        saved[attId] = { status: 'completed', markdown: lite, isHtml: !!state.isHtml, imagesStripped: true }
      }
      localStorage.setItem('parseStates', JSON.stringify(saved))
    }
  } catch (e) { console.warn('保存解析状态失败:', e) }
}

function restoreParseStates() {
  try {
    const saved = JSON.parse(localStorage.getItem('parseStates') || '{}')
    for (const [attId, data] of Object.entries(saved)) {
      if (data.status === 'completed' && data.markdown) {
        parseStates[attId] = { status: 'completed', progress: '解析完成', markdown: data.markdown, isHtml: !!data.isHtml }
      }
    }
  } catch (e) { console.warn('恢复解析状态失败:', e) }
}

function canParse(att) {
  const ext = getFileExt(att)
  return ['pdf', 'png', 'jpg', 'jpeg', 'docx', 'doc', 'zip'].includes(ext)
}

async function handleZipAttachment(att) {
  // 获取 ZIP 文件
  let file = attachmentFileCache.get(att.id)
  if (!file) file = await loadAttachmentFile(att, 'file.zip')
  if (!file) {
    ElMessage.error('未找到后端附件文件，请先确认附件文件已在后端持久化')
    return
  }

  try {
    const zip = await JSZip.loadAsync(file)
    const parseableExts = ['pdf', 'png', 'jpg', 'jpeg', 'docx', 'doc']
    const files = []
    zip.forEach((relativePath, zipEntry) => {
      if (zipEntry.dir) return
      // 跳过 macOS 资源文件
      if (relativePath.startsWith('__MACOSX/') || relativePath.includes('/.')) return
      const name = relativePath
      const ext = name.includes('.') ? name.split('.').pop().toLowerCase() : ''
      files.push({
        name,
        path: relativePath,
        size: zipEntry._data?.uncompressedSize || 0,
        ext,
        parseable: parseableExts.includes(ext),
        parsing: false,
        parsed: false,
        parseResult: '',
        isHtml: false
      })
    })
    // 按文件名排序，可解析的排前面
    files.sort((a, b) => {
      if (a.parseable && !b.parseable) return -1
      if (!a.parseable && b.parseable) return 1
      return a.name.localeCompare(b.name)
    })
    // 恢复之前的解析结果
    restoreZipParseStates(att.id, files)
    zipFileList.value = files
    zipInstance.value = zip
    zipParentAtt.value = att
    zipContentsAttName.value = att.displayName
    showZipContentsDialog.value = true
  } catch (e) {
    ElMessage.error('ZIP 解压失败: ' + (e.message || e))
  }
}

async function parseZipEntry(zf) {
  if (!zipInstance.value) return
  zf.parsing = true
  try {
    const zipEntry = zipInstance.value.file(zf.path)
    if (!zipEntry) throw new Error('文件不存在: ' + zf.path)

    const ext = zf.ext
    if (ext === 'docx' || ext === 'doc') {
      // DOCX: 提取为 blob，发送到后端解析
      const blob = await zipEntry.async('blob')
      const file = new File([blob], zf.name.split('/').pop(), {
        type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
      })
      const result = await attachmentApi.parseDocx(file)
      zf.parseResult = result.html || ''
      zf.isHtml = true
    } else if (ext === 'pdf' || ['png', 'jpg', 'jpeg'].includes(ext)) {
      const blob = await zipEntry.async('blob')
      const mimeMap = { pdf: 'application/pdf', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg' }
      const file = new File([blob], zf.name.split('/').pop(), { type: mimeMap[ext] || 'application/octet-stream' })
      const submitResult = await parseApi.submit(file, { return_images: true })
      const taskId = submitResult.task_id
      if (!taskId) throw new Error('未返回任务ID')

      // 轮询
      let pollCount = 0
      const maxPolls = 300
      while (pollCount++ < maxPolls) {
        await new Promise(r => setTimeout(r, 2000))
        const statusResult = await parseApi.getStatus(taskId)
        const taskStatus = statusResult.status
        
        // 更新进度提示
        if (taskStatus === 'pending') {
          zf.parseProgress = `任务排队中... (${pollCount}/${maxPolls})`
        } else if (taskStatus === 'processing') {
          zf.parseProgress = statusResult.progress || `正在解析中... (${pollCount}/${maxPolls})`
        }
        
        if (taskStatus === 'completed') {
          const result = await parseApi.getResult(taskId)
          let markdown = result.markdown || ''
          // 尝试从 zip 提取图片（如果 markdown 引用了 images/）
          const imageRefs = markdown.match(/!\[[^\]]*\]\(images\/[^)]+\)/g)
          if (imageRefs && imageRefs.length > 0) {
            try {
              const zipBlob = await parseApi.downloadZip(taskId)
              const imgZip = await JSZip.loadAsync(zipBlob)
              for (const ref of imageRefs) {
                const match = ref.match(/!\[([^\]]*)\]\((images\/[^)]+)\)/)
                if (!match) continue
                const [, , imgPath] = match
                let imgFile = imgZip.file(imgPath)
                if (!imgFile) {
                  const fn = imgPath.split('/').pop()
                  const cands = imgZip.file(new RegExp(fn.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$'))
                  if (cands.length > 0) imgFile = cands[0]
                }
                if (imgFile) {
                  const imgData = await imgFile.async('base64')
                  const imgExt = imgPath.split('.').pop().toLowerCase()
                  const mime = imgExt === 'png' ? 'image/png' : imgExt === 'gif' ? 'image/gif' : 'image/jpeg'
                  markdown = markdown.replace(`(${imgPath})`, `(data:${mime};base64,${imgData})`)
                }
              }
            } catch (zipErr) { console.warn('提取解析图片失败:', zipErr) }
          }
          zf.parseResult = markdown
          zf.isHtml = false
          break
        } else if (statusResult.status === 'failed') {
          throw new Error('解析失败')
        }
      }
      if (!zf.parseResult) throw new Error('解析超时')
    }
    zf.parsed = true
    zf.parsing = false
    // 持久化 ZIP 解析结果
    if (zipParentAtt.value) saveZipParseStates(zipParentAtt.value.id)
    ElMessage.success(`「${zf.name.split('/').pop()}」解析完成`)
  } catch (e) {
    zf.parsing = false
    ElMessage.error(`解析失败: ${e.message || e}`)
  }
}

function viewZipEntryResult(zf) {
  parseResultAttName.value = zf.name.split('/').pop()
  parseResultContent.value = zf.parseResult
  parseResultIsHtml.value = !!zf.isHtml
  parseResultPreviewAvailable.value = !!zipInstance.value
  parseResultOriginZf.value = zf
  previewContentType.value = '' // 重置，切换到原件时才加载
  parseResultViewMode.value = 'rendered'
  showParseResultDialog.value = true
}

function saveZipParseStates(attId) {
  try {
    const allZip = JSON.parse(localStorage.getItem('zipParseStates') || '{}')
    const entries = {}
    for (const zf of zipFileList.value) {
      if (zf.parsed && zf.parseResult) {
        entries[zf.path] = { parseResult: zf.parseResult, isHtml: !!zf.isHtml }
      }
    }
    allZip[attId] = entries
    try {
      localStorage.setItem('zipParseStates', JSON.stringify(allZip))
    } catch (quotaErr) {
      console.warn('localStorage 空间不足，保存 ZIP 解析精简版')
      // 去掉 base64 图片数据再存
      for (const key of Object.keys(entries)) {
        entries[key].parseResult = entries[key].parseResult
          .replace(/data:[^;]+;base64,[A-Za-z0-9+/=]+/g, '')
          .replace(/src="data:[^"]+"/g, 'src=""')
        entries[key].imagesStripped = true
      }
      allZip[attId] = entries
      localStorage.setItem('zipParseStates', JSON.stringify(allZip))
    }
  } catch (e) { console.warn('保存 ZIP 解析状态失败:', e) }
}

function restoreZipParseStates(attId, files) {
  try {
    const allZip = JSON.parse(localStorage.getItem('zipParseStates') || '{}')
    const entries = allZip[attId]
    if (!entries) return
    for (const zf of files) {
      const saved = entries[zf.path]
      if (saved && saved.parseResult) {
        zf.parsed = true
        zf.parseResult = saved.parseResult
        zf.isHtml = !!saved.isHtml
      }
    }
  } catch (e) { console.warn('恢复 ZIP 解析状态失败:', e) }
}

async function previewZipEntry(zf) {
  // 直接打开解析结果弹窗，默认切到原件预览模式
  parseResultAttName.value = zf.name.split('/').pop()
  parseResultContent.value = zf.parseResult || ''
  parseResultIsHtml.value = !!zf.isHtml
  parseResultPreviewAvailable.value = true
  parseResultOriginZf.value = zf
  parseResultViewMode.value = 'preview'
  showParseResultDialog.value = true
  await loadPreviewFromZip(zf)
}

async function switchToPreviewMode() {
  parseResultViewMode.value = 'preview'
  if (previewContentType.value) return // 已加载
  const zf = parseResultOriginZf.value
  if (zf) {
    await loadPreviewFromZip(zf)
  } else if (parseResultOriginAtt.value) {
    await loadPreviewFromAtt(parseResultOriginAtt.value)
  }
}

async function loadPreviewFromAtt(att) {
  // 清理旧的 blob URL
  if (previewContentUrl.value) {
    URL.revokeObjectURL(previewContentUrl.value)
    previewContentUrl.value = ''
  }
  const ext = getFileExt(att)
  try {
    // 获取原始文件：优先缓存，其次后端文件服务
    const file = await loadAttachmentFile(att)
    if (!file) {
      ElMessage.warning('原始文件不可用，请确认后端附件文件已持久化')
      previewContentType.value = 'unsupported'
      return
    }

    if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) {
      previewContentUrl.value = URL.createObjectURL(file)
      previewContentType.value = 'image'
    } else if (ext === 'pdf') {
      const typedBlob = new Blob([file], { type: 'application/pdf' })
      previewContentUrl.value = URL.createObjectURL(typedBlob)
      previewContentType.value = 'pdf'
    } else if (ext === 'docx' || ext === 'doc') {
      // DOCX 已解析的直接用解析结果
      const state = parseStates[att.id]
      if (state?.isHtml && state.markdown) {
        previewContentHtml.value = state.markdown
      } else {
        const result = await attachmentApi.parseDocx(file)
        previewContentHtml.value = result.html || '<p>解析结果为空</p>'
      }
      previewContentType.value = 'html'
    } else {
      previewContentType.value = 'unsupported'
    }
  } catch (e) {
    ElMessage.error('预览失败: ' + (e.message || e))
  }
}

async function loadPreviewFromZip(zf) {
  if (!zipInstance.value) return
  const ext = zf.ext
  try {
    const zipEntry = zipInstance.value.file(zf.path)
    if (!zipEntry) { ElMessage.warning('文件不存在'); return }

    // 清理旧的 blob URL
    if (previewContentUrl.value) {
      URL.revokeObjectURL(previewContentUrl.value)
      previewContentUrl.value = ''
    }

    if (['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg'].includes(ext)) {
      const blob = await zipEntry.async('blob')
      const mimeMap = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', bmp: 'image/bmp', webp: 'image/webp', svg: 'image/svg+xml' }
      const typedBlob = new Blob([blob], { type: mimeMap[ext] || 'image/png' })
      previewContentUrl.value = URL.createObjectURL(typedBlob)
      previewContentType.value = 'image'
    } else if (ext === 'pdf') {
      const blob = await zipEntry.async('blob')
      const typedBlob = new Blob([blob], { type: 'application/pdf' })
      previewContentUrl.value = URL.createObjectURL(typedBlob)
      previewContentType.value = 'pdf'
    } else if (ext === 'docx' || ext === 'doc') {
      if (zf.parsed && zf.parseResult && zf.isHtml) {
        previewContentHtml.value = zf.parseResult
      } else {
        const blob = await zipEntry.async('blob')
        const file = new File([blob], zf.name.split('/').pop(), {
          type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
        })
        const result = await attachmentApi.parseDocx(file)
        previewContentHtml.value = result.html || '<p>解析结果为空</p>'
      }
      previewContentType.value = 'html'
    } else if (['txt', 'md', 'csv', 'json', 'xml', 'yml', 'yaml', 'log', 'ini', 'cfg', 'conf', 'sh', 'bat', 'py', 'java', 'js', 'ts', 'html', 'css'].includes(ext)) {
      const text = await zipEntry.async('string')
      previewContentText.value = text
      previewContentType.value = 'text'
    } else {
      previewContentType.value = 'unsupported'
    }
  } catch (e) {
    ElMessage.error('预览失败: ' + (e.message || e))
  }
}

function cleanupPreviewContent() {
  if (previewContentUrl.value) {
    URL.revokeObjectURL(previewContentUrl.value)
    previewContentUrl.value = ''
  }
  previewContentHtml.value = ''
  previewContentText.value = ''
  previewContentType.value = ''
  parseResultPreviewAvailable.value = false
  parseResultOriginAtt.value = null
  parseResultOriginZf.value = null
  highlightSourceText.value = '' // 清除高亮文本
}

async function handleParseAttachment(att) {
  const state = getParseState(att.id)
  if (state.status === 'uploading' || state.status === 'parsing') {
    ElMessage.warning('该附件正在解析中，请稍候')
    return
  }

  const ext = getFileExt(att)
  if (!canParse(att)) {
    ElMessage.warning('仅支持 PDF、DOCX、ZIP 和图片文件的解析')
    return
  }

  // ZIP 文件走解压展示流程
  if (ext === 'zip') {
    await handleZipAttachment(att)
    return
  }

  try {
    // 1. 获取后端持久化的原始文件
    state.status = 'uploading'
    state.progress = '正在准备文件...'

    state.progress = '正在获取文件...'
    const file = await loadAttachmentFile(att, `file.${ext}`)
    if (!file) {
      state.status = 'failed'
      state.progress = '未找到后端附件文件'
      ElMessage.error('未找到后端附件文件，请先确认附件文件已在后端持久化')
      return
    }

    // 2. DOCX 走后端 Java 解析，PDF/图片走 GPU 解析服务
    if (ext === 'docx' || ext === 'doc') {
      state.status = 'parsing'
      state.progress = '正在解析 DOCX 文件...'
      ElMessage.info(`附件「${att.displayName}」开始解析`)
      try {
        const result = await attachmentApi.parseDocx(file)
        const html = result.html || ''
        state.markdown = html
        state.status = 'completed'
        state.progress = '解析完成'
        state.isHtml = true

        if (typeof att.id === 'number' || /^\d+$/.test(att.id)) {
          try { await attachmentApi.saveParsedContent(att.id, html) } catch (e) { console.warn('后端持久化失败:', e) }
        }
        att.parsed = true
        att.parsedText = html
        saveParseState(att.id)
        ElMessage.success(`附件「${att.displayName}」解析完成`)
      } catch (docxErr) {
        state.status = 'failed'
        state.progress = '解析失败'
        ElMessage.error(`DOCX 解析失败: ${docxErr.message || docxErr}`)
      }
      return
    }

    // PDF/图片：提交解析任务（启用图片返回）
    state.progress = '正在提交解析任务...'
    const submitResult = await parseApi.submit(file, { return_images: true })
    const taskId = submitResult.task_id
    if (!taskId) throw new Error('未返回任务ID')

    // 3. 轮询任务状态
    state.status = 'parsing'
    state.progress = '解析中...'
    state.taskId = taskId
    ElMessage.info(`附件「${att.displayName}」开始解析`)

    const maxPolls = 300  // 最多轮询 300 次 (约 10 分钟)
    let pollCount = 0
    const pollInterval = 2000 // 2秒一次

    const poll = async () => {
      pollCount++
      if (pollCount > maxPolls) {
        state.status = 'failed'
        state.progress = '解析超时'
        ElMessage.error('解析超时，请稍后重试')
        return
      }

      try {
        const statusResult = await parseApi.getStatus(taskId)
        const taskStatus = statusResult.status

        if (taskStatus === 'completed') {
          // 获取解析结果
          state.progress = '正在获取结果...'
          const result = await parseApi.getResult(taskId)
          let markdown = result.markdown || ''

          // 下载 zip 包提取图片，转为 base64 内嵌到 markdown
          const imageRefs = markdown.match(/!\[[^\]]*\]\(images\/[^)]+\)/g)
          if (imageRefs && imageRefs.length > 0) {
            state.progress = '正在获取图片...'
            try {
              const zipBlob = await parseApi.downloadZip(taskId)
              const zip = await JSZip.loadAsync(zipBlob)
              // 遍历所有图片引用，替换为 base64 data URL
              for (const ref of imageRefs) {
                const match = ref.match(/!\[([^\]]*)\]\((images\/[^)]+)\)/)
                if (!match) continue
                const [, alt, imgPath] = match
                // zip 中图片路径可能带前缀目录，尝试多种匹配
                let imgFile = zip.file(imgPath)
                if (!imgFile) {
                  // 尝试在 zip 中搜索文件名
                  const fileName = imgPath.split('/').pop()
                  const candidates = zip.file(new RegExp(fileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '$'))
                  if (candidates.length > 0) imgFile = candidates[0]
                }
                if (imgFile) {
                  const imgData = await imgFile.async('base64')
                  const ext = imgPath.split('.').pop().toLowerCase()
                  const mime = ext === 'png' ? 'image/png' : ext === 'gif' ? 'image/gif' : 'image/jpeg'
                  markdown = markdown.replace(`(${imgPath})`, `(data:${mime};base64,${imgData})`)
                }
              }
            } catch (zipErr) {
              console.warn('下载 zip 提取图片失败:', zipErr)
            }
          }

          state.markdown = markdown
          state.status = 'completed'
          state.progress = '解析完成'

          // 持久化解析结果到后端（仅真实附件，mock 附件 ID 为字符串跳过）
          if (typeof att.id === 'number' || /^\d+$/.test(att.id)) {
            try {
              await attachmentApi.saveParsedContent(att.id, markdown)
            } catch (e) {
              console.warn('后端持久化解析结果失败:', e)
            }
          }

          // 更新附件对象标记已解析
          att.parsed = true
          att.parsedText = markdown

          // 持久化到 localStorage（刷新后恢复）
          saveParseState(att.id)

          ElMessage.success(`附件「${att.displayName}」解析完成`)
        } else if (taskStatus === 'failed') {
          const errMsg = statusResult.error || '解析失败'
          state.status = 'failed'
          state.progress = errMsg
          ElMessage.error(`解析失败: ${errMsg}`)
        } else {
          // pending / processing
          let progressMsg = ''
          if (taskStatus === 'pending') {
            progressMsg = `任务排队中，请稍候... (${pollCount}/${maxPolls})`
          } else if (taskStatus === 'processing') {
            progressMsg = statusResult.progress || `正在解析中... (${pollCount}/${maxPolls})`
          } else {
            progressMsg = `解析中... (${pollCount}/${maxPolls})`
          }
          state.progress = progressMsg
          
          // 每30秒提示一次用户任务仍在处理
          if (pollCount % 15 === 0) {
            ElMessage.info(`解析任务仍在处理中，已等待 ${Math.floor(pollCount * 2 / 60)} 分钟，请耐心等待...`)
          }
          
          setTimeout(poll, pollInterval)
        }
      } catch (e) {
        state.status = 'failed'
        state.progress = '查询状态失败'
        ElMessage.error('查询解析状态失败: ' + e.message)
      }
    }

    setTimeout(poll, pollInterval)
  } catch (e) {
    state.status = 'failed'
    state.progress = '解析失败'
    ElMessage.error('解析失败: ' + e.message)
  }
}

// 规则
async function handleCreateRule() {
  if (!newRuleForm.ruleName) return
  try {
    const rule = await ruleApi.create(currentProjectId.value, { ...newRuleForm })
    rules.value.push(rule); showNewRuleDialog.value = false
    Object.assign(newRuleForm, { 
      ruleName: '', 
      ruleType: 'direct_entity', 
      targetElementKey: '',
      sourceAttachmentId: null,
      locatorType: 'full_text',
      chapterTitle: '',
      reviewCode: '',
      tableSelector: '',
      prompt: ''
    })
    ElMessage.success('规则创建成功')
  } catch (e) { ElMessage.error('创建失败: ' + e.message) }
}
async function handleDeleteRule(rule) {
  try {
    await ElMessageBox.confirm(`确定删除规则「${rule.ruleName}」？`, '删除确认', { type: 'warning' })
    await ruleApi.delete(rule.id); rules.value = rules.value.filter(r => r.id !== rule.id); ElMessage.success('规则已删除')
  } catch (e) { if (e !== 'cancel') ElMessage.error('删除失败') }
}
async function handleExecuteRule(rule) {
  // 弹出规则详情预览
  pendingExecuteRule.value = rule
  showRuleEngineDialog.value = true
}

async function confirmExecuteSingleRule() {
  const rule = pendingExecuteRule.value
  if (!rule) return
  showRuleEngineDialog.value = false
  rule._executing = true
  try { 
    await ruleApi.execute(rule.id)
    ElMessage.success(`规则「${rule.ruleName}」执行成功`)
    await loadProjectData(currentProjectId.value) 
  }
  catch (e) { ElMessage.error('执行失败: ' + e.message) }
  finally { 
    rule._executing = false
    pendingExecuteRule.value = null
  }
}
async function handleBatchExecuteRules() {
  if (!currentProjectId.value) return
  // 先弹出规则引擎数据预览（批量模式）
  pendingExecuteRule.value = null  // 清空单条规则，表示批量模式
  if (ruleEngineData.value.length > 0) {
    showRuleEngineDialog.value = true
  } else {
    ElMessage.warning('没有可执行的自动规则')
  }
}

async function confirmExecuteRules() {
  if (!currentProjectId.value) return
  showRuleEngineDialog.value = false
  executingRules.value = true
  try { 
    await ruleApi.batchExecute(currentProjectId.value)
    ElMessage.success('批量执行完成')
    await loadProjectData(currentProjectId.value) 
  }
  catch (e) { ElMessage.error('执行失败: ' + e.message) }
  finally { executingRules.value = false }
}

// 工作流保存
async function handleWorkflowSave(workflowData) {
  if (!currentProjectId.value || !workflowData) return
  
  try {
    const rulesToCreate = convertWorkflowToRules(workflowData)
    
    for (const ruleDTO of rulesToCreate) {
      await ruleApi.create(currentProjectId.value, ruleDTO)
    }
    
    ElMessage.success(`成功创建 ${rulesToCreate.length} 条规则`)
    showRuleWorkflow.value = false
    await loadProjectData(currentProjectId.value)
  } catch (e) {
    ElMessage.error('保存失败: ' + e.message)
  }
}

function convertWorkflowToRules(workflowData) {
  const { nodes, edges } = workflowData
  const rules = []
  
  const elementNodes = nodes.filter(n => n.type === 'element')
  
  // 辅助函数：递归查找数据流路径
  function traceDataFlow(nodeId, visited = new Set()) {
    if (visited.has(nodeId)) return { sources: [], actions: [] }
    visited.add(nodeId)
    
    const node = nodes.find(n => n.id === nodeId)
    if (!node) return { sources: [], actions: [] }
    
    if (node.type === 'source') {
      return { sources: [node], actions: [] }
    }
    
    if (node.type === 'action') {
      const inEdges = edges.filter(e => e.target === nodeId)
      let allSources = []
      let allActions = [node]
      
      for (const edge of inEdges) {
        const upstream = traceDataFlow(edge.source, visited)
        allSources = [...allSources, ...upstream.sources]
        allActions = [...allActions, ...upstream.actions]
      }
      
      return { sources: allSources, actions: allActions }
    }
    
    return { sources: [], actions: [] }
  }
  
  for (const elementNode of elementNodes) {
    if (!elementNode.data.elementKey) continue
    
    const incomingEdges = edges.filter(e => e.target === elementNode.id)
    if (incomingEdges.length === 0) continue
    
    // 收集所有输入路径
    let allSources = []
    let allActions = []
    
    for (const edge of incomingEdges) {
      const { sources, actions } = traceDataFlow(edge.source)
      allSources = [...allSources, ...sources]
      allActions = [...allActions, ...actions]
    }
    
    // 去重
    const uniqueSources = [...new Map(allSources.map(s => [s.id, s])).values()]
    const uniqueActions = [...new Map(allActions.map(a => [a.id, a])).values()]
    
    // 确定主要动作（最接近输出的动作节点）
    const directInputEdge = incomingEdges[0]
    const directInputNode = nodes.find(n => n.id === directInputEdge.source)
    
    let primaryAction = null
    if (directInputNode?.type === 'action') {
      primaryAction = directInputNode
    } else if (uniqueActions.length > 0) {
      primaryAction = uniqueActions[0]
    }
    
    const actionType = primaryAction?.data?.actionType || primaryAction?.data?.subType || 'quote'
    const prompt = primaryAction?.data?.prompt || ''
    
    // 构建输入列表
    const inputs = uniqueSources.map(source => ({
      sourceNodeId: source.data.sourceNodeId || null,
      inputType: source.data.subType || 'attachment',
      inputName: source.data.sourceName || source.data.label || '来源',
      sourceText: source.data.sourceText || null,
      locatorType: source.data.locatorType || 'full_text',
      chapterTitle: source.data.chapterTitle || null,
      reviewCode: source.data.reviewCode || null
    })).filter(inp => inp.sourceNodeId || inp.sourceText)
    
    // 构建动作配置
    const actionConfig = {
      locatorType: uniqueSources[0]?.data?.locatorType || 'full_text',
      chapterTitle: uniqueSources[0]?.data?.chapterTitle || null,
      reviewCode: uniqueSources[0]?.data?.reviewCode || null,
      tableSelector: primaryAction?.data?.tableSelector || null,
      prompt: prompt || null,
      outputFormat: primaryAction?.data?.outputFormat || 'text'
    }
    
    // 如果有多个动作节点，记录动作链
    if (uniqueActions.length > 1) {
      actionConfig.actionChain = uniqueActions.map(a => ({
        actionType: a.data.actionType || a.data.subType,
        prompt: a.data.prompt || null
      }))
    }
    
    const rule = {
      elementKey: elementNode.data.elementKey,
      ruleName: `${elementNode.data.elementName || elementNode.data.elementKey}-${getActionLabel(actionType)}`,
      ruleType: 'extraction',
      actionType: actionType,
      actionConfig: JSON.stringify(actionConfig),
      inputs: inputs
    }
    
    rules.push(rule)
  }
  
  return rules
}

function getActionLabel(actionType) {
  const labels = {
    quote: '引用',
    summary: 'AI总结',
    ai_extract: 'AI提取',
    table_extract: '表格提取'
  }
  return labels[actionType] || actionType
}

// 工具函数
function formatTime(dateStr) {
  if (!dateStr) return ''
  const date = new Date(dateStr); const now = new Date()
  const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24))
  if (diffDays === 0) return '今天'
  if (diffDays === 1) return '昨天'
  if (diffDays < 7) return `${diffDays}天前`
  return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
}
function getStatusText(status) {
  const map = { 'draft': '草稿', 'active': '进行中', 'archived': '已归档', 'completed': '已完成' }
  return map[status] || '草稿'
}

onMounted(async () => {
  await loadProjects()
  const pid = route.query.project
  if (pid) {
    const p = projects.value.find(p => String(p.id) === String(pid))
    if (p) await switchProject(p)
  }
})
</script>

<style lang="scss" scoped>
// ==========================================
// Editor 页面样式 - 参考 V2 原型设计
// ==========================================

.editor-page {
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}


.editor-body {
  flex: 1;
  display: flex;
  overflow: hidden;
}

// ==========================================
// 拖拽分隔条
// ==========================================
.resize-handle {
  width: 4px;
  background: transparent;
  cursor: col-resize;
  flex-shrink: 0;
  position: relative;
  z-index: 10;
  transition: background 0.2s;
  
  &:hover, &:active {
    background: var(--primary);
  }
  
  &::before {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: -3px;
    right: -3px;
  }
}

// ==========================================
// 左侧面板 - 参考设计风格
// ==========================================
.left-panel {
  background: var(--white);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  min-width: 260px;
  max-width: 420px;
  overflow: hidden;
  position: relative;

  // ---- 顶部 Logo ----
  .sidebar-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 18px 12px;
    flex-shrink: 0;

    .sidebar-logo {
      display: flex;
      align-items: center;
      gap: 8px;

      .logo-icon {
        font-size: 22px;
        color: var(--primary);
        line-height: 1;
      }

      .logo-text {
        font-size: 17px;
        font-weight: 700;
        color: var(--text-1);
        letter-spacing: 0.5px;
      }
    }

    .sidebar-header-actions {
      display: flex;
      gap: 4px;
      color: var(--text-3);
    }
  }

  // ---- 快捷导航 ----
  .sidebar-nav {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 0 14px 10px;
    flex-shrink: 0;

    .nav-item {
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 10px 14px;
      border-radius: 10px;
      cursor: pointer;
      transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
      font-size: 14px;
      color: var(--text-1);
      border: 1.5px solid transparent;

      &:hover {
        background: linear-gradient(135deg, #f8fbff 0%, #f0f7ff 100%);
        border-color: #e6f0ff;
        transform: translateX(2px);
        box-shadow: 0 2px 8px rgba(64, 158, 255, 0.06);
      }

      .nav-icon {
        font-size: 18px;
        flex-shrink: 0;
      }

      .nav-label {
        font-weight: 600;
        letter-spacing: 0.2px;
      }
    }
  }

  // ---- 区块通用 ----
  .sidebar-section {
    display: flex;
    flex-direction: column;
    padding: 0 14px;
    flex-shrink: 0;

    .section-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 10px 4px 8px;

      .section-title {
        font-size: 13px;
        font-weight: 600;
        color: var(--text-2);
      }

      .section-action {
        font-size: 12px;
        color: var(--text-3);
        cursor: pointer;
        transition: color 0.15s;

        &:hover {
          color: var(--primary);
        }
      }
    }

    .sidebar-search {
      margin-bottom: 10px;

      :deep(.el-input__wrapper) {
        border-radius: 10px;
        background: linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%);
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
        border: 1.5px solid #e8ecf0;
        transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);

        &:hover {
          border-color: #409eff;
          background: #fff;
          box-shadow: 0 2px 8px rgba(64, 158, 255, 0.08);
        }
        
        &.is-focus {
          border-color: #409eff;
          background: #fff;
          box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
        }
      }
    }
  }

  // ---- 文档列表 ----
  .doc-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
    overflow-y: auto;
    max-height: 340px;
    padding-bottom: 4px;
  }

  .doc-item {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 12px;
    border-radius: 12px;
    cursor: pointer;
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    position: relative;
    border: 1.5px solid transparent;
    background: transparent;

    &:hover {
      background: linear-gradient(135deg, #f8fbff 0%, #f0f7ff 100%);
      transform: translateY(-1px);
      box-shadow: 0 2px 8px rgba(64, 158, 255, 0.08);
      
      .doc-icon-wrap {
        transform: scale(1.05);
        box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
      }
    }

    &.active {
      background: linear-gradient(135deg, #e6f0ff 0%, #d9e9ff 100%);
      border-color: var(--primary);
      box-shadow: 0 2px 12px rgba(64, 158, 255, 0.12);

      .doc-item-title {
        color: var(--primary);
        font-weight: 700;
      }
      
      .doc-icon-wrap {
        background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
        
        .doc-icon-glyph {
          filter: brightness(0) invert(1);
        }
      }
    }

    .doc-icon-wrap {
      width: 40px;
      height: 40px;
      border-radius: 10px;
      background: linear-gradient(135deg, #eef3ff 0%, #e1ecff 100%);
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
      box-shadow: 0 2px 6px rgba(64, 158, 255, 0.06);

      .doc-icon-glyph {
        font-size: 20px;
        transition: filter 0.2s;
      }
    }

    .doc-item-body {
      flex: 1;
      min-width: 0;

      .doc-item-title {
        font-size: 13px;
        font-weight: 600;
        color: var(--text-1);
        line-height: 1.4;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
        margin-bottom: 4px;
      }

      .doc-item-meta {
        display: flex;
        align-items: center;
        gap: 6px;
        font-size: 11px;
        color: var(--text-3);
        flex-wrap: wrap;

        .doc-status-tag {
          font-size: 10px;
          height: 18px;
          line-height: 16px;
          padding: 0 6px;
          border-radius: 4px;
        }

        .doc-item-date {
          white-space: nowrap;
        }

        .doc-item-author {
          white-space: nowrap;
        }
      }
      
      .doc-item-progress {
        display: flex;
        flex-direction: column;
        gap: 4px;
        
        .el-progress {
          width: 100%;
        }
        
        .progress-text {
          font-size: 11px;
          color: var(--primary);
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
        }
      }
    }

    .doc-more-btn {
      opacity: 0;
      flex-shrink: 0;
      transition: opacity 0.15s;
      color: var(--text-3);
      margin-top: 2px;
    }

    &:hover .doc-more-btn {
      opacity: 1;
    }
  }

  .doc-empty {
    padding: 20px;
    text-align: center;
    color: var(--text-3);
    font-size: 13px;
  }

  .doc-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 20px;
    color: var(--text-3);
    font-size: 13px;
  }

  // ---- 最近操作 ----
  .sidebar-activity {
    flex: 1;
    min-height: 0;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    border-top: 1px solid var(--border);
    margin-top: 6px;
    padding-top: 4px;

    .activity-list {
      flex: 1;
      overflow-y: auto;
      display: flex;
      flex-direction: column;
      gap: 2px;
    }

    .activity-item {
      padding: 8px 12px;
      border-radius: 8px;
      transition: background 0.15s;

      &:hover {
        background: var(--bg);
      }

      .activity-text {
        font-size: 13px;
        color: var(--text-1);
        line-height: 1.4;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .activity-meta {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-top: 3px;
        font-size: 11px;
        color: var(--text-3);

        .activity-source {
          color: var(--primary);
        }
      }
    }

    .activity-empty {
      padding: 20px;
      text-align: center;
      color: var(--text-3);
      font-size: 12px;
    }
  }

  // ---- 底部用户栏 ----
  .sidebar-footer {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px;
    border-top: 1px solid var(--border);
    flex-shrink: 0;
    background: var(--white);

    .user-avatar {
      width: 34px;
      height: 34px;
      border-radius: 50%;
      background: linear-gradient(135deg, var(--primary), #69c0ff);
      color: #fff;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 14px;
      font-weight: 700;
      flex-shrink: 0;
    }

    .user-info {
      flex: 1;
      min-width: 0;
      display: flex;
      flex-direction: column;

      .user-name {
        font-size: 13px;
        font-weight: 600;
        color: var(--text-1);
        line-height: 1.3;
      }

      .user-role {
        font-size: 11px;
        color: var(--text-3);
        display: flex;
        align-items: center;
        gap: 4px;

        &::before {
          content: '';
          width: 6px;
          height: 6px;
          border-radius: 50%;
          background: #52c41a;
          flex-shrink: 0;
        }
      }
    }

    .footer-actions {
      display: flex;
      align-items: center;
      gap: 4px;

      :deep(.el-button.is-circle) {
        width: 32px;
        height: 32px;
      }

      :deep(.el-icon) {
        font-size: 18px;
      }

      :deep(.el-icon svg) {
        width: 18px;
        height: 18px;
      }

      .notification-badge {
        :deep(.el-badge__content) {
          font-size: 10px;
          height: 16px;
          line-height: 16px;
          padding: 0 4px;
        }
      }
    }
  }

  // ---- 覆盖层面板（附件/规则） ----
  .sidebar-overlay-panel {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--white);
    z-index: 20;
    display: flex;
    flex-direction: column;

    .overlay-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 12px 14px;
      border-bottom: 1px solid var(--border);
      flex-shrink: 0;

      .overlay-title {
        font-size: 14px;
        font-weight: 600;
        color: var(--text-1);
      }
    }

    .overlay-body {
      flex: 1;
      overflow-y: auto;
      padding: 12px;
    }
  }

  // 覆盖层滑入动画
  .slide-right-enter-active {
    transition: transform 0.25s ease-out;
  }

  .slide-right-leave-active {
    transition: transform 0.2s ease-in;
  }

  .slide-right-enter-from {
    transform: translateX(-100%);
  }

  .slide-right-leave-to {
    transform: translateX(-100%);
  }
}

// ==========================================
// 上传区 - V2 风格
// ==========================================
.upload-zone {
  border: 2px dashed var(--border);
  border-radius: var(--radius-lg);
  margin-bottom: 16px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--white);
  transition: all 0.2s;

  &:hover {
    border-color: var(--primary);
    background: var(--primary-light);
  }

  :deep(.el-upload-dragger) {
    padding: 0 12px;
    border: none;
    background: transparent;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .upload-content {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .upload-icon {
    font-size: 18px;
  }

  .upload-text {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-1);
  }

  .upload-hint {
    display: block;
    font-size: 11px;
    color: var(--text-3);
    margin-top: 8px;
    text-align: center;
  }
}

.file-list {
  margin-bottom: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

// ==========================================
// 文件项 - V2 风格
// ==========================================
.file-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px;
  background: var(--white);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all 0.2s;
  position: relative;

  &:hover {
    border-color: var(--primary);
    background: var(--primary-light);
  }
  
  &.active {
    border-color: var(--primary);
    background: var(--primary-light);
  }

  .file-icon {
    width: 40px;
    height: 40px;
    border-radius: var(--radius-sm);
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-weight: 700;
    font-size: 13px;
    flex-shrink: 0;
    
    &.pdf { background: #ff6b6b; }
    &.docx, &.doc { background: #4dabf7; }
    &.xlsx, &.xls { background: #73d13d; }
    &.md { background: #9254de; }
    &.default { background: var(--text-3); }
  }

  .file-info {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;

    .file-name {
      font-size: 13px;
      font-weight: 600;
      color: var(--text-1);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    .file-meta {
      font-size: 11px;
      color: var(--text-3);
      margin-top: 4px;

      .required {
        color: var(--danger);
      }
    }
  }
  
  .file-status {
    font-size: 11px;
    white-space: nowrap;
    
    &.parsing { color: var(--primary); }
    &.done { color: var(--success); }
  }
}

.add-source-btn {
  width: 100%;
  border-radius: var(--radius-md);
}

// ==========================================
// 附件面板 - V2 风格
// ==========================================
.att-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  .att-count {
    font-size: 13px;
    color: var(--text-2);
    font-weight: 500;
  }
}

.att-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.att-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: all 0.15s;
  position: relative;

  &:hover {
    background: #f5f7fa;
    .att-more-btn { opacity: 1; }
    .att-parse-btn { opacity: 1; }
  }

  &.active {
    background: var(--primary-light);
  }

  .att-icon {
    width: 36px;
    height: 36px;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-weight: 700;
    font-size: 11px;
    flex-shrink: 0;
    letter-spacing: -0.5px;

    &.type-pdf { background: #e74c3c; }
    &.type-word { background: #3b82f6; }
    &.type-excel { background: #22c55e; }
    &.type-image { background: #f59e0b; font-size: 16px; }
    &.type-archive { background: #8b5cf6; }
    &.type-other { background: #94a3b8; }
  }

  .att-info {
    flex: 1;
    min-width: 0;

    .att-name {
      font-size: 13px;
      font-weight: 500;
      color: var(--text-1);
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      line-height: 1.4;
    }

    .att-meta {
      display: flex;
      align-items: center;
      gap: 6px;
      margin-top: 2px;
      font-size: 11px;
      color: var(--text-3);

      .att-type {
        color: var(--primary);
        font-weight: 500;
      }

      .att-size {
        &::before {
          content: '·';
          margin-right: 6px;
          color: var(--text-3);
        }
      }
    }
  }

  .att-parse-btn {
    flex-shrink: 0;
    font-size: 12px;
    padding: 2px 8px;
  }

  .att-more-btn {
    opacity: 0;
    transition: opacity 0.15s;
    flex-shrink: 0;
  }

  .att-parse-status {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    font-size: 11px;
    margin-left: 4px;

    &.parsing {
      color: #e6a23c;
    }
    &.completed {
      color: #67c23a;
    }
    &.failed {
      color: #f56c6c;
    }
  }
}

// ==========================================
// 中间面板 - V2 风格
// ==========================================
.center-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: var(--white);
  overflow: hidden;
  border-radius: var(--radius-md);
  margin: 0 8px;
  box-shadow: var(--shadow-sm);

  // ==========================================
  // 欢迎页 - V2 风格
  // ==========================================
  .welcome-page {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--white);
    
    .welcome-content {
      text-align: center;
      max-width: 600px;
      padding: 48px;
    }
    
    .welcome-logo {
      width: 80px;
      height: 80px;
      margin: 0 auto 32px;
      background: linear-gradient(135deg, var(--primary) 0%, #69c0ff 100%);
      border-radius: 16px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 40px;
      font-weight: 700;
      color: white;
      box-shadow: 0 12px 32px rgba(24, 144, 255, 0.3);
    }
    
    .welcome {
      h1 {
        font-size: 28px;
        font-weight: 700;
        color: var(--text-1);
        margin-bottom: 12px;
        line-height: 1.4;
        
        span {
          display: block;
          font-size: 20px;
          font-weight: 500;
          background: var(--ai-gradient);
          background-clip: text;
          -webkit-background-clip: text;
          -webkit-text-fill-color: transparent;
          margin-top: 8px;
        }
      }
      
      p {
        font-size: 15px;
        color: var(--text-3);
        line-height: 1.6;
      }
      
      .welcome-version {
        margin-top: 24px;
        font-size: 12px;
        color: var(--text-4, #bbb);
      }
    }
  }

  // ==========================================
  // 编辑器标题栏 - 参考设计风格
  // ==========================================
  .editor-title-bar {
    padding: 0 20px;
    height: 54px;
    border-bottom: 1.5px solid #e8ecf0;
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
    flex-shrink: 0;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);

    .titlebar-left {
      display: flex;
      align-items: center;
      gap: 8px;
      min-width: 0;
      flex: 1;

      .titlebar-folder-icon {
        font-size: 20px;
        color: #909399;
        flex-shrink: 0;
      }

      .titlebar-sep {
        color: #d0d7de;
        font-size: 16px;
        flex-shrink: 0;
        font-weight: 300;
      }

      .titlebar-project-name {
        font-size: 15px;
        font-weight: 700;
        color: #1f2937;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        max-width: 360px;
        letter-spacing: 0.2px;
      }

      .titlebar-status-tag {
        flex-shrink: 0;
        margin-left: 6px;
        border-radius: 6px;
        font-size: 11px;
        font-weight: 600;
        padding: 3px 10px;
        background: linear-gradient(135deg, #e6f0ff 0%, #d9e9ff 100%);
        color: #409eff;
        border: 1px solid #d9e9ff;
      }
    }

    .titlebar-right {
      display: flex;
      align-items: center;
      gap: 6px;
      flex-shrink: 0;

      .titlebar-save-status {
        display: flex;
        align-items: center;
        gap: 6px;
        font-size: 12px;
        color: #606266;
        white-space: nowrap;
        margin-right: 6px;
        padding: 6px 12px;
        border-radius: 8px;
        background: #f5f7fa;
        font-weight: 500;

        .save-dot {
          width: 7px;
          height: 7px;
          border-radius: 50%;
          background: #d0d7de;
          box-shadow: 0 0 0 2px rgba(208, 215, 222, 0.2);

          &.saved {
            background: #52c41a;
            box-shadow: 0 0 0 2px rgba(82, 196, 26, 0.2);
          }
        }
      }

      :deep(.el-button.is-circle) {
        width: 36px;
        height: 36px;
        border-radius: 10px;
        transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
        border: 1.5px solid transparent;
        
        &:hover {
          background: linear-gradient(135deg, #f0f7ff 0%, #e6f0ff 100%);
          border-color: #d9e9ff;
          transform: translateY(-1px);
          box-shadow: 0 2px 8px rgba(64, 158, 255, 0.08);
        }
      }

      :deep(.el-icon) {
        font-size: 18px;
      }

      :deep(.el-button.is-active-view) {
        color: white;
        background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
        border-color: #409eff;
        box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
        
        &:hover {
          background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
          transform: translateY(-1px);
          box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
        }
      }

      :deep(.el-divider--vertical) {
        height: 24px;
        margin: 0 6px;
        background: #e8ecf0;
      }
    }
  }

  // ==========================================
  // 编辑器滚动区 - V2 风格
  // ==========================================
  .editor-scroll {
    flex: 1;
    overflow-y: auto;
    padding: 40px 48px;
    background: var(--white);
    position: relative;
  }

  .editor-content {
    max-width: 1000px;
    margin: 0 auto;
    outline: none;
    
    // 文档块样式
    :deep(.doc-block) {
      position: relative;
      transition: background-color 0.2s;
      
      &:hover {
        background-color: rgba(24, 144, 255, 0.02);
      }
      
      // 被选中时的样式
      &.selected {
        background-color: rgba(24, 144, 255, 0.08);
        outline: 1px dashed var(--primary);
      }
    }

    :deep(h1) {
      font-size: 24px;
      font-weight: 700;
      margin-bottom: 24px;
    }

    :deep(h2) {
      font-size: 18px;
      font-weight: 600;
      margin: 28px 0 16px;
    }

    :deep(p) {
      margin-bottom: 12px;
      line-height: 1.6;
    }

    :deep(ul) {
      margin-bottom: 16px;
      padding-left: 24px;

      li {
        margin-bottom: 8px;
      }
    }
    
    // 目录样式
    :deep(.doc-toc-title) {
      font-size: 18pt;
      font-weight: bold;
      text-align: center;
      margin: 20px 0 16px;
    }
    
    :deep(.doc-toc-item) {
      display: flex;
      align-items: baseline;
      padding: 6px 0;
      line-height: 1.6;
      cursor: pointer;
      transition: background-color 0.2s;
      
      &:hover {
        background-color: #f5f5f5;
      }
      
      .toc-title {
        flex-shrink: 0;
        white-space: nowrap;
      }
      
      .toc-dots {
        flex: 1;
        border-bottom: 1px dotted #999;
        margin: 0 8px;
        min-width: 20px;
        height: 0.6em;
      }
      
      .toc-page {
        flex-shrink: 0;
        color: #666;
        min-width: 20px;
        text-align: right;
      }
    }
    
    // 表格样式
    :deep(.doc-table-container) {
      margin: 16px 0;
      overflow-x: auto;
    }
    
    :deep(.doc-table) {
      width: 100%;
      border-collapse: collapse;
      font-size: 14px;
      
      th, td {
        border: 1px solid #ddd;
        padding: 8px 12px;
        text-align: left;
        vertical-align: top;
        line-height: 1.5;
      }
      
      th {
        background-color: #f5f5f5;
        font-weight: bold;
      }
      
      tr:nth-child(even) td {
        background-color: #fafafa;
      }
      
      tr:hover td {
        background-color: #f0f7ff;
      }
    }
    
    :deep(.doc-table-empty) {
      padding: 20px;
      text-align: center;
      color: #999;
      border: 1px dashed #ddd;
      margin: 16px 0;
    }
    
    // 列表项样式
    :deep(.doc-list-item) {
      position: relative;
      margin-bottom: 8px;
      line-height: 1.6;
      
      &.bullet {
        padding-left: 1.5em;
        &::before {
          content: '•';
          position: absolute;
          left: 0;
        }
      }
      
      &.ordered {
        padding-left: 2em;
        counter-increment: doc-list;
        &::before {
          content: counter(doc-list) '.';
          position: absolute;
          left: 0;
        }
      }
    }
    
    // 重置列表计数器
    :deep(p + .doc-list-item.ordered:first-of-type),
    :deep(.doc-list-item.bullet + .doc-list-item.ordered) {
      counter-reset: doc-list;
    }
    
    // 块引用样式
    :deep(blockquote) {
      margin: 16px 0;
      padding: 12px 20px;
      border-left: 4px solid #ddd;
      background: #f9f9f9;
      color: #666;
    }
    
    // 代码块样式
    :deep(pre) {
      margin: 16px 0;
      padding: 16px;
      background: #f5f5f5;
      border-radius: 4px;
      overflow-x: auto;
      
      code {
        font-family: 'Consolas', 'Monaco', monospace;
        font-size: 13px;
      }
    }
    
    // 实体高亮样式 - 匹配原型 UI（边框 + 背景）
    :deep(.entity-highlight) {
      display: inline;
      padding: 2px 8px;
      border-radius: 4px;
      cursor: pointer;
      transition: all 0.2s;
      font-weight: 500;
      border: 1px solid #1890ff;
      color: #1890ff;
      background: rgba(24, 144, 255, 0.1);
      
      &:hover {
        background: #1890ff;
        color: white;
      }
      
      // 实体类型颜色
      &.entity {
        border-color: #1890ff;
        color: #1890ff;
        background: rgba(24, 144, 255, 0.1);
        &:hover { background: #1890ff; color: white; }
      }
      
      &.concept {
        border-color: #722ed1;
        color: #722ed1;
        background: rgba(114, 46, 209, 0.1);
        &:hover { background: #722ed1; color: white; }
      }
      
      &.data {
        border-color: #52c41a;
        color: #52c41a;
        background: rgba(82, 196, 26, 0.1);
        &:hover { background: #52c41a; color: white; }
      }
      
      &.location {
        border-color: #faad14;
        color: #d48806;
        background: rgba(250, 173, 20, 0.1);
        &:hover { background: #faad14; color: white; }
      }
      
      &.asset {
        border-color: #eb2f96;
        color: #eb2f96;
        background: rgba(235, 47, 150, 0.1);
        &:hover { background: #eb2f96; color: white; }
      }
      
      &.person {
        border-color: #1890ff;
        color: #1890ff;
        background: rgba(24, 144, 255, 0.1);
        &:hover { background: #1890ff; color: white; }
      }
      
      &.org {
        border-color: #722ed1;
        color: #722ed1;
        background: rgba(114, 46, 209, 0.1);
        &:hover { background: #722ed1; color: white; }
      }
      
      &.date {
        border-color: #13c2c2;
        color: #13c2c2;
        background: rgba(19, 194, 194, 0.1);
        &:hover { background: #13c2c2; color: white; }
      }
      
      &.product {
        border-color: #eb2f96;
        color: #eb2f96;
        background: rgba(235, 47, 150, 0.1);
        &:hover { background: #eb2f96; color: white; }
      }
      
      &.event {
        border-color: #fa8c16;
        color: #fa8c16;
        background: rgba(250, 140, 22, 0.1);
        &:hover { background: #fa8c16; color: white; }
      }
      
      &.law {
        border-color: #2f54eb;
        color: #2f54eb;
        background: rgba(47, 84, 235, 0.1);
        &:hover { background: #2f54eb; color: white; }
      }
      
      // 未确认的 AI 建议（文档中虚线样式）
      &.ai-suggestion-pending {
        border-style: dashed;
        opacity: 0.9;
      }
      
      // 点击 AI 建议后，文档中该要素的「待确认」高亮
      &.entity-pending-confirm {
        box-shadow: 0 0 0 2px #1890ff;
        opacity: 1;
      }
    }
  }
}

// ==========================================
// 右侧面板 - 参考设计风格
// ==========================================
.right-panel {
  background: var(--white);
  border-left: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  min-width: 280px;
  max-width: 420px;
  overflow: hidden;

  // ---- 报告要素区 ----
  .rp-elements {
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    max-height: 45%;
    border-bottom: 1px solid var(--border);
  }

  .rp-elements-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 14px 8px;
    flex-shrink: 0;

    .rp-elements-title {
      display: flex;
      align-items: center;
      gap: 6px;

      .rp-title-icon { font-size: 15px; }
      .rp-title-text { font-size: 13px; font-weight: 600; color: var(--text-1); }
      .rp-title-count {
        font-size: 11px;
        color: #fff;
        background: var(--el-color-primary);
        padding: 1px 6px;
        border-radius: 10px;
        font-weight: 500;
      }
    }
    
    .rp-header-actions {
      display: flex;
      align-items: center;
      gap: 2px;
    }
  }

  .rp-elements-body {
    flex: 1;
    overflow-y: auto;
    padding: 0 8px 12px;
  }

  .rp-element-list {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }

  .rp-element-group {
    .rp-group-header {
      display: flex;
      align-items: center;
      gap: 6px;
      padding: 6px 8px;
      cursor: pointer;
      border-radius: 4px;
      transition: background 0.15s;
      
      &:hover { background: var(--bg-2); }
      
      .rp-group-icon {
        font-size: 10px;
        color: var(--text-3);
        width: 12px;
      }
      .rp-group-name {
        font-size: 12px;
        font-weight: 600;
        color: var(--text-2);
        flex: 1;
      }
      .rp-group-count {
        font-size: 10px;
        color: var(--text-3);
        background: var(--bg-2);
        padding: 1px 5px;
        border-radius: 8px;
      }
    }
    
    .rp-group-items {
      padding-left: 12px;
    }
  }

  .rp-element-item {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 5px 8px;
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.15s;
    border-left: 2px solid transparent;
    
    &:hover {
      background: var(--bg-2);
    }
    
    &.is-active {
      background: #e6f4ff;
      border-left-color: var(--el-color-primary);
    }
    
    &.has-value {
      .rp-item-name { color: var(--text-1); }
    }
    
    .rp-item-type {
      font-size: 9px;
      font-weight: 600;
      width: 14px;
      height: 14px;
      display: flex;
      align-items: center;
      justify-content: center;
      border-radius: 3px;
      flex-shrink: 0;
    }
    
    &.is-text .rp-item-type {
      background: #e6f7ff;
      color: #1890ff;
    }
    &.is-paragraph .rp-item-type {
      background: #f6ffed;
      color: #52c41a;
    }
    &.is-table .rp-item-type {
      background: #fff7e6;
      color: #fa8c16;
    }
    
    .rp-item-name {
      font-size: 12px;
      color: var(--text-3);
      flex: 1;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    
    .rp-item-preview {
      font-size: 11px;
      color: var(--text-3);
      max-width: 80px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      opacity: 0.7;
    }
  }

  .rp-elements-empty {
    padding: 20px;
    text-align: center;
    color: var(--text-3);
    font-size: 12px;
  }

  // ---- AI 助手区 ----
  .rp-ai {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;
  }

  .rp-ai-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 16px 8px;
    flex-shrink: 0;

    .rp-ai-title {
      display: flex;
      align-items: center;
      gap: 6px;
      font-size: 14px;
      font-weight: 700;
      color: var(--text-1);

      .rp-ai-icon { font-size: 16px; }
    }

    .rp-ai-actions {
      display: flex;
      gap: 4px;
    }
  }

  .rp-ai-messages {
    flex: 1;
    overflow-y: auto;
    padding: 8px 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
  }

  .ai-message {
    display: flex;

    &.ai-user {
      justify-content: flex-end;

      .ai-bubble {
        background: var(--primary);
        color: #fff;
        border-radius: 16px 16px 4px 16px;
      }
    }

    &.ai-bot {
      justify-content: flex-start;

      .ai-bubble {
        background: #f4f5f7;
        color: var(--text-1);
        border-radius: 16px 16px 16px 4px;
      }
    }
  }

  .ai-bubble {
    max-width: 85%;
    padding: 10px 14px;
    font-size: 13px;
    line-height: 1.6;
    word-break: break-word;
  }

  .rp-ai-input {
    flex-shrink: 0;
    padding: 10px 14px 12px;
    border-top: 1px solid var(--border);

    :deep(.el-textarea__inner) {
      border-radius: 12px;
      background: #f7f8fa;
      border: 1px solid var(--border);
      padding: 10px 14px;
      font-size: 13px;
      line-height: 1.5;

      &:focus {
        border-color: var(--primary);
        background: var(--white);
      }
    }
  }

  .rp-ai-input-actions {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 6px;
    padding: 0 2px;

    :deep(.el-button.is-circle) {
      width: 32px;
      height: 32px;
      font-size: 18px;
    }

    :deep(.el-button--primary.is-circle) {
      width: 34px;
      height: 34px;
    }

    :deep(.el-icon) {
      font-size: 18px;
    }

    :deep(.el-icon svg) {
      width: 18px;
      height: 18px;
    }
  }

  .rp-ai-input-tools,
  .rp-ai-input-right {
    display: flex;
    align-items: center;
    gap: 4px;
  }
}

// ==========================================
// 要素管理区 - V2 风格
// ==========================================
.element-section {
  padding: 16px;
  border-bottom: 1px dashed var(--border);
  
  // 模块标题样式 - V2 风格
  .module-title {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 15px;
    font-weight: 700;
    color: var(--text-1);
    margin-bottom: 14px;
    
    .module-icon {
      width: 36px;
      height: 36px;
      border-radius: 8px;
      background: var(--primary-gradient);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 18px;
      color: white;
      box-shadow: var(--shadow-md);
    }
  }

  .element-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;

    .element-title {
      font-size: 13px;
      font-weight: 600;
      display: flex;
      align-items: center;
      gap: 6px;

      .element-count {
        font-size: 11px;
        color: var(--text-3);
        font-weight: normal;
      }
    }
    
    .header-actions {
      display: flex;
      gap: 4px;
      
      .el-button {
        padding: 4px 8px;
        font-size: 12px;
      }
    }
  }
  
  // AI 建议区块特殊样式
  &.ai-section {
    background: var(--bg);
    border-bottom: none;
    
    .element-header {
      .element-title {
        color: var(--text-2);
      }
    }
    
    .element-option.ai-highlight-option {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 8px 16px;
      font-size: 12px;
      color: var(--text-2);
      .option-label {
        flex: 1;
      }
    }
    
    .element-tags-wrap {
      max-height: 300px;
    }
  }
    
  // 要素 Tab 切换 - V2 风格
  .element-tabs {
    display: flex;
    gap: 8px;
    
    .element-tab {
      padding: 6px 12px;
      border-radius: 12px;
      background: transparent;
      border: 1px solid transparent;
      font-size: 13px;
      cursor: pointer;
      color: var(--text-2);
      transition: all 0.2s;
      
      &:hover {
        background: var(--bg);
      }
      
      &.active {
        background: var(--primary);
        color: #fff;
        border-color: rgba(0, 0, 0, 0.04);
        box-shadow: var(--shadow-md);
      }
    }
  }

  .element-filter {
    padding: 0 0 12px;
    
    .entity-search {
      margin-bottom: 12px;
      
      :deep(.el-input__wrapper) {
        border-radius: 18px;
        background: var(--bg);
        box-shadow: none;
        border: 1px solid var(--border);
        
        &:hover, &.is-focus {
          border-color: var(--primary);
          background: var(--white);
        }
      }
    }
    
    .entity-type-filter {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      
      .filter-tag {
        cursor: pointer;
        transition: all 0.2s;
        border-radius: 12px;
        font-size: 11px;
        
        &:hover {
          border-color: var(--primary);
          color: var(--primary);
        }
        
        &.active {
          background: var(--primary);
          color: white;
          border-color: var(--primary);
        }
        
        &.clear {
          background: transparent;
          border-style: dashed;
          color: var(--text-3);
          
          &:hover {
            border-color: var(--danger);
            color: var(--danger);
          }
        }
      }
    }
  }

  .element-body {
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 16px;
  }

  // 要素标签容器 - V2 风格
  .element-tags-wrap {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    max-height: 200px;
    overflow-y: auto;
    padding-right: 4px;
    padding-bottom: 16px;
    
    &::-webkit-scrollbar {
      width: 4px;
    }
    
    &::-webkit-scrollbar-track {
      background: var(--bg);
      border-radius: 2px;
    }
    
    &::-webkit-scrollbar-thumb {
      background: var(--border);
      border-radius: 2px;
      
      &:hover {
        background: var(--text-3);
      }
    }
  }
  
  // ==========================================
  // 要素标签样式 - V2 风格
  // ==========================================
  .var-tag {
    height: 28px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 0 12px;
    border-radius: 2px;
    font-size: 12px;
    cursor: pointer;
    transition: all 0.2s;
    background: var(--bg);
    border: 1px solid var(--border);
    user-select: none;
    
    &:hover {
      border-color: var(--primary);
      background: var(--primary-light);
      transform: translateY(-1px);
    }
    
    &:active {
      cursor: grabbing;
    }
    
    .tag-icon {
      font-size: 12px;
    }
    
    .tag-name {
      max-width: 120px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      font-weight: 500;
      line-height: 28px;
    }
    
    .tag-status {
      color: #52c41a;
      font-size: 10px;
    }
    
    .tag-action {
      color: var(--primary);
      font-size: 14px;
      font-weight: bold;
      margin-left: 2px;
    }
    
    // 已确认的要素
    &.confirmed {
      background: var(--white);
      border-color: var(--primary);
      
      .tag-name {
        color: var(--text-1);
      }
    }
    
    // AI 建议的要素（虚线边框、淡色）
    &.ai-suggestion {
      background: transparent;
      border-style: dashed;
      border-color: var(--border);
      opacity: 0.85;
      
      .tag-name {
        color: var(--text-2);
      }
      
      &:hover {
        opacity: 1;
        border-color: var(--primary);
        border-style: solid;
        background: var(--primary-light);
        
        .tag-action {
          transform: scale(1.2);
        }
      }
    }
    
    // 动态要素样式（圆角）
    &.dynamic {
      border-radius: 14px;
    }
    
    // 静态要素样式（微圆角）
    &.static {
      border-radius: 2px;
    }
    
    // 已确认状态
    &.confirmed {
      background: rgba(82, 196, 26, 0.1);
      border-color: #52c41a;
      
      .tag-name {
        color: #389e0d;
      }
    }
    
    // 实体类型样式 - 左边框颜色区分
    &.entity-person, &.entity {
      border-left: 3px solid var(--primary);
    }
    &.entity-org, &.concept {
      border-left: 3px solid #722ed1;
    }
    &.entity-location, &.location {
      border-left: 3px solid var(--warning);
    }
    &.entity-date {
      border-left: 3px solid #13c2c2;
    }
    &.entity-data, &.data {
      border-left: 3px solid var(--success);
    }
    &.entity-product, &.asset {
      border-left: 3px solid #eb2f96;
    }
    &.entity-event {
      border-left: 3px solid #fa8c16;
    }
    &.entity-law {
      border-left: 3px solid #2f54eb;
    }
    &.entity-default {
      border-left: 3px solid #8c8c8c;
    }
    
    // 当前正在确认的 AI 建议 tag
    &.is-pending {
      border-color: var(--primary);
      background: var(--primary-light);
      border-style: solid;
    }
  }
  
  // AI 建议确认栏已移至「+」按钮的悬浮框内，此处样式仅作保留注释

  .element-hint {
    font-size: 12px;
    color: var(--text-3);
    text-align: center;
    padding: 24px;
  }
}

// 实体高亮闪烁效果
@keyframes entity-flash {
  0%, 100% { background-color: inherit; }
  50% { background-color: #ffe58f; }
}

.entity-highlight-flash {
  animation: entity-flash 0.5s ease-in-out 3;
}

// 实体编辑弹窗样式
.entity-edit-form {
  .entity-edit-preview {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 16px;
    background: var(--primary-light);
    border: 1px dashed var(--primary);
    border-radius: 8px;
    margin-bottom: 20px;
    
    .preview-icon {
      font-size: 24px;
    }
    
    .preview-text {
      font-size: 16px;
      font-weight: 600;
      color: var(--primary);
    }
  }
}

.category-section {
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);

  .category-header {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 12px;
    font-weight: 600;
    margin-bottom: 10px;

    .category-dot {
      width: 10px;
      height: 10px;
      border-radius: 50%;
    }

    .category-count {
      color: var(--text-3);
      font-weight: normal;
      background: var(--bg);
      padding: 2px 8px;
      border-radius: 10px;
    }
  }

  .category-items {
    .category-item {
      display: flex;
      justify-content: space-between;
      padding: 8px 12px;
      background: var(--bg);
      border-radius: 6px;
      margin-bottom: 6px;
      cursor: pointer;
      font-size: 12px;
      transition: all 0.2s;

      &:hover {
        background: var(--primary-light);
      }

      .item-value {
        color: var(--text-3);
      }
    }
  }
}

// ==========================================
// 右键菜单 - V2 风格
// ==========================================
.context-menu {
  position: fixed;
  min-width: 180px;
  background: var(--white);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  z-index: 3000;
  overflow: hidden;

  .context-menu-header {
    padding: 12px 14px;
    background: var(--bg);
    border-bottom: 1px solid var(--border);
    
    .selected-preview {
      font-size: 12px;
      color: var(--primary);
      font-weight: 600;
      max-width: 150px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }
  
  .context-menu-section {
    padding: 8px 14px 4px;
    font-size: 10px;
    color: var(--text-3);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
  }

  .context-menu-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 14px;
    font-size: 13px;
    cursor: pointer;
    transition: all 0.15s;
    color: var(--text-1);
    position: relative;

    &:hover {
      background: var(--primary-light);
      color: var(--primary);
    }
    
    &[disabled="true"] {
      opacity: 0.5;
      pointer-events: none;
    }

    .icon {
      font-size: 14px;
      width: 20px;
      text-align: center;
      flex-shrink: 0;
    }
    
    .shortcut {
      margin-left: auto;
      font-size: 11px;
      color: var(--text-3);
    }
    
    .submenu-arrow {
      margin-left: auto;
      font-size: 14px;
      color: var(--text-3);
    }
    
    &.has-submenu {
      &:hover .submenu-arrow {
        color: var(--primary);
      }
    }
  }
  
  // 子菜单
  .context-submenu {
    position: absolute;
    left: 100%;
    top: 0;
    min-width: 150px;
    background: var(--white);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
    
    .context-menu-item {
      padding: 8px 12px;
      font-size: 12px;
      gap: 8px;
      
      .icon {
        font-size: 12px;
        width: 16px;
      }
    }
  }
  
  .context-menu-divider {
    height: 1px;
    background: var(--border);
    margin: 4px 0;
  }
  
  .context-menu-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 12px;
    color: var(--primary);
    font-size: 12px;
    border-top: 1px solid var(--border);
    background: var(--bg);
  }
}

// ==========================================
// 实体弹出框样式 - V2 风格
// ==========================================
.entity-popover {
  .entity-popover-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
    
    .entity-text {
      font-weight: 600;
      font-size: 14px;
      max-width: 140px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      color: var(--text-1);
    }
  }
  
  .entity-popover-type {
    font-size: 12px;
    color: var(--text-2);
    margin-bottom: 14px;
    padding: 4px 8px;
    background: var(--bg);
    border-radius: 4px;
    display: inline-block;
  }
  
  .entity-popover-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    
    :deep(.el-button) {
      border-radius: var(--radius-sm);
    }
  }
}

// ==========================================
// 知识图谱容器 - V2 风格
// ==========================================
.graph-container {
  height: 500px;
  position: relative;
  background: linear-gradient(45deg, #f8f9fa 25%, transparent 25%), 
              linear-gradient(-45deg, #f8f9fa 25%, transparent 25%), 
              linear-gradient(45deg, transparent 75%, #f8f9fa 75%), 
              linear-gradient(-45deg, transparent 75%, #f8f8f8 75%);
  background-size: 20px 20px;
  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
  border-radius: var(--radius-md);

  .graph-legend {
    position: absolute;
    top: 16px;
    left: 16px;
    background: var(--white);
    border-radius: var(--radius-md);
    padding: 14px 18px;
    box-shadow: var(--shadow-md);

    .legend-title {
      font-size: 12px;
      font-weight: 600;
      margin-bottom: 10px;
      color: var(--text-1);
    }

    .legend-item {
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 12px;
      color: var(--text-2);
      margin-bottom: 6px;
      
      &:last-child {
        margin-bottom: 0;
      }
    }

    .legend-dot {
      width: 12px;
      height: 12px;
      border-radius: 50%;

      &.core, &.entity { background: var(--primary); }
      &.concept { background: #722ed1; }
      &.data { background: var(--success); }
      &.location { background: var(--warning); }
    }
  }

  .graph-body {
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;

    .graph-placeholder {
      text-align: center;
      color: var(--text-3);
      
      .placeholder-icon {
        font-size: 48px;
        margin-bottom: 16px;
        opacity: 0.5;
      }

      p {
        margin-top: 12px;
        font-size: 14px;
      }
    }
  }
}

// ==========================================
// 空白编辑器占位提示样式 - V2 风格
// ==========================================
:deep(.empty-editor-placeholder) {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 80px 40px;
  text-align: center;
  min-height: 400px;

  .empty-icon {
    font-size: 64px;
    margin-bottom: 24px;
    opacity: 0.8;
  }

  h2 {
    font-size: 24px;
    font-weight: 600;
    margin-bottom: 12px;
    color: var(--text-1);
  }

  .empty-subtitle {
    font-size: 15px;
    color: var(--text-3);
    margin-bottom: 32px;
  }

  .empty-actions {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin-bottom: 32px;
    width: 100%;
    max-width: 400px;
  }

  .action-card {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 16px 20px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: all 0.2s;
    text-align: left;

    &:hover {
      border-color: var(--primary);
      background: var(--primary-light);
      transform: translateX(4px);
    }

    .action-icon {
      font-size: 24px;
      flex-shrink: 0;
    }

    .action-text {
      font-size: 14px;
      color: var(--text-1);
      font-weight: 500;
    }
  }

  .empty-hint {
    font-size: 13px;
    color: var(--text-3);
    padding: 12px 20px;
    background: var(--bg);
    border-radius: var(--radius-md);
    border-left: 3px solid var(--primary);
  }
}

// 高亮块动画
.highlight-block {
  animation: highlight-pulse 2s ease-out;
}

@keyframes highlight-pulse {
  0% {
    background: rgba(24, 144, 255, 0.3);
    box-shadow: 0 0 0 4px rgba(24, 144, 255, 0.2);
  }
  100% {
    background: transparent;
    box-shadow: none;
  }
}

// ==========================================
// 报告要素管理弹窗样式
// ==========================================
.elements-modal {
  :deep(.el-dialog__header) {
    padding: 16px 20px;
    border-bottom: 1px solid var(--border);
    margin-right: 0;
  }
  
  :deep(.el-dialog__body) {
    padding: 0;
  }
  
  :deep(.el-dialog__footer) {
    padding: 12px 20px;
    border-top: 1px solid var(--border);
  }
}

.elements-modal-content {
  .elements-search {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 16px 20px;
    border-bottom: 1px solid var(--border);
    background: var(--bg);
    
    .el-input {
      max-width: 300px;
    }
  }
  
  .elements-table-wrap {
    padding: 0;
    
    :deep(.el-table) {
      .element-name {
        font-weight: 500;
        color: var(--text-1);
      }
      
      .element-desc {
        color: var(--text-3);
        font-size: 12px;
      }
      
      .original-value {
        color: var(--text-2);
        font-size: 12px;
      }
      
      .element-source {
        color: var(--primary);
        font-size: 12px;
      }
      
      .el-input__wrapper {
        box-shadow: none;
        background: var(--bg);
        border-radius: var(--radius-sm);
        
        &:hover, &.is-focus {
          background: var(--white);
          box-shadow: 0 0 0 1px var(--primary);
        }
      }
    }
  }
  
  .elements-pagination {
    display: flex;
    justify-content: flex-end;
    padding: 12px 20px;
    border-top: 1px solid var(--border);
  }
}

// ==========================================
// DOCX上传区域样式
// ==========================================
.upload-docx-area {
  .el-upload__tip {
    font-size: 12px;
    color: var(--text-3);
    margin-top: 6px;
  }
  
  :deep(.el-upload-list) {
    max-width: 100%;
  }
  
  :deep(.el-upload-list__item) {
    max-width: 100%;
  }
  
  :deep(.el-upload-list__item-file-name) {
    max-width: 280px;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.parse-message {
  font-size: 12px;
  color: var(--text-2);
  margin-top: 6px;
}

// ==========================================
// 新建报告对话框样式
// ==========================================
.new-report-dialog {
  :deep(.el-dialog__header) {
    padding: 16px 20px;
    border-bottom: 1px solid var(--border);
    margin-right: 0;
  }
  
  :deep(.el-dialog__body) {
    padding: 20px;
  }
  
  :deep(.el-dialog__footer) {
    padding: 12px 20px;
    border-top: 1px solid var(--border);
  }
}

.new-report-form {
  .section-label {
    font-size: 13px;
    font-weight: 500;
    color: var(--text-2);
    margin-bottom: 10px;
  }
  
  .create-type-section {
    margin-bottom: 20px;
  }
  
  .create-type-options {
    display: flex;
    gap: 12px;
  }
  
  .type-option {
    flex: 1;
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px;
    background: var(--bg);
    border: 2px solid var(--border);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: all 0.2s;
    position: relative;
    
    &:hover {
      border-color: var(--primary-light);
      background: var(--white);
    }
    
    &.active {
      border-color: var(--primary);
      background: var(--primary-light);
      
      .option-title {
        color: var(--primary);
      }
    }
    
    .option-icon {
      font-size: 24px;
      flex-shrink: 0;
      line-height: 1;
    }
    
    .option-content {
      flex: 1;
      min-width: 0;
    }
    
    .option-title {
      font-size: 14px;
      font-weight: 600;
      color: var(--text-1);
      margin-bottom: 4px;
    }
    
    .option-desc {
      font-size: 12px;
      color: var(--text-3);
      line-height: 1.4;
    }
    
    .option-check {
      position: absolute;
      top: 8px;
      right: 8px;
      width: 20px;
      height: 20px;
      background: var(--primary);
      color: white;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 12px;
      font-weight: bold;
    }
  }
  
  .name-input-section {
    margin-bottom: 20px;
    
    :deep(.el-input__wrapper) {
      border-radius: var(--radius-sm);
    }
  }
  
  .upload-section {
    .report-upload-area {
      :deep(.el-upload) {
        width: 100%;
      }
      
      :deep(.el-upload-dragger) {
        width: 100%;
        height: auto;
        padding: 24px;
        border-radius: var(--radius-md);
        border: 2px dashed var(--border);
        background: var(--bg);
        
        &:hover {
          border-color: var(--primary);
          background: var(--primary-light);
        }
      }
      
      .upload-content {
        text-align: center;
      }
      
      .upload-icon {
        font-size: 32px;
        color: var(--text-3);
        margin-bottom: 8px;
      }
      
      .upload-text {
        font-size: 14px;
        color: var(--text-2);
        margin-bottom: 4px;
        
        em {
          color: var(--primary);
          font-style: normal;
        }
      }
      
      .upload-hint {
        font-size: 12px;
        color: var(--text-3);
      }
    }
  }
}

// ==========================================
// 要素视图
// ==========================================
.elements-view {
  padding: 24px;
  
  .elements-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
    gap: 16px;
  }
  
  .element-card {
    background: #fff;
    border: 1.5px solid #e8ecf0;
    border-radius: 12px;
    overflow: hidden;
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
    
    &:hover {
      border-color: #409eff;
      box-shadow: 0 4px 12px rgba(64, 158, 255, 0.08);
      transform: translateY(-1px);
    }
    
    .element-card-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 14px 16px;
      background: linear-gradient(135deg, #fafbfc 0%, #f8fafc 100%);
      border-bottom: 1.5px solid #e8ecf0;
      
      .element-label {
        font-weight: 700;
        font-size: 14px;
        color: #1f2937;
        letter-spacing: 0.2px;
      }
    }
    
    .element-card-body {
      padding: 12px 16px;
      
      .element-value-row {
        margin-bottom: 8px;
        
        .value-meta {
          margin-top: 4px;
          font-size: 11px;
          color: var(--text-3);
          
          .original-label {
            margin-right: 4px;
          }
          .original-value {
            color: var(--text-2);
          }
        }
        
        .value-status {
          margin-top: 4px;
        }
      }
      
      .element-empty {
        padding: 16px 0;
        text-align: center;
        
        .empty-hint {
          font-size: 12px;
          color: var(--text-3);
        }
      }
    }
  }
  
  .elements-empty {
    padding: 60px 20px;
    text-align: center;
  }
}

// ==========================================
// 实体视图
// ==========================================
.entities-view {
  padding: 24px;
  
  .entity-filter-bar {
    display: flex;
    align-items: center;
    margin-bottom: 16px;
  }
  
  .entities-empty {
    padding: 60px 20px;
    text-align: center;
  }
}

// ==========================================
// 项目概览统计
// ==========================================
.overview-stats {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  
  .stat-item {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 12px 8px;
    background: var(--bg);
    border-radius: var(--radius-sm);
    
    .stat-label {
      font-size: 11px;
      color: var(--text-3);
      margin-bottom: 4px;
    }
    
    .stat-value {
      font-size: 20px;
      font-weight: 700;
      color: var(--text-1);
      
      &.filled {
        color: var(--success);
      }
    }
  }
}

// ==========================================
// 文档视图 - Word 排版还原 + 可编辑 + 要素高亮
// ==========================================
.document-view {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
  background: #e8e8e8;
  min-height: 100%;
  position: relative;

  .doc-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 16px;
    padding: 8px 16px;
    background: var(--white);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    width: 100%;
    max-width: 820px;

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

    .doc-title-label {
      font-size: 14px;
      font-weight: 600;
      color: var(--text-1);
      white-space: nowrap;
    }
  }

  .doc-paper {
    background: #fff;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
    border-radius: 2px;
    width: 100%;
    max-width: 820px;
    min-height: 1100px;
    padding: 96px 90px;
    font-family: 'Times New Roman', 'SimSun', '宋体', serif;
    font-size: 12pt;
    line-height: 1.6;
    color: #000;
    word-wrap: break-word;
    overflow-wrap: break-word;
  }

  :deep(.doc-block) {
    margin: 0;
    padding: 0;
    min-height: 1em;
  }

  // 标题样式
  :deep(h1.doc-block) {
    font-size: 18pt;
    font-weight: bold;
    margin: 16px 0 10px;
    line-height: 1.4;
  }

  :deep(h2.doc-block) {
    font-size: 16pt;
    font-weight: bold;
    margin: 14px 0 8px;
    line-height: 1.4;
  }

  :deep(h3.doc-block) {
    font-size: 14pt;
    font-weight: bold;
    margin: 12px 0 6px;
    line-height: 1.4;
  }

  // 段落
  :deep(p.doc-block) {
    margin: 0 0 2px;
    text-align: justify;
  }

  // 目录
  :deep(.doc-toc1), :deep(.doc-toc2), :deep(.doc-toc3) {
    position: relative;
    font-size: 13pt;
    margin: 0;
    padding: 8px 16px;
    cursor: pointer;
    border-left: 3px solid transparent;
    transition: background 0.15s, border-color 0.15s;

    &:hover {
      background: #f0f7ff;
      border-left-color: #1890ff;
    }
  }
  :deep(.doc-toc1) {
    font-weight: 600;
    color: #1f2937;
  }
  :deep(.doc-toc2) {
    padding-left: 36px;
    font-size: 12pt;
    color: #374151;
  }
  :deep(.doc-toc3) {
    padding-left: 56px;
    font-size: 11pt;
    color: #6b7280;
  }

  // 空段落
  :deep(p.doc-paragraph:empty::after),
  :deep(p.doc-block:empty::after) {
    content: '\00a0';
  }

  // 要素模板 {{key}} 标签
  :deep(.elem-tpl-tag) {
    display: inline;
    background: #fff7e6;
    color: #d46b08;
    border: 1.5px solid #ffc069;
    border-radius: 3px;
    padding: 1px 6px;
    font-family: 'Consolas', 'Monaco', monospace;
    font-size: 0.9em;
    font-weight: 600;
    white-space: nowrap;
    cursor: default;
    user-select: all;

    &:hover {
      background: #ffe7ba;
      border-color: #fa8c16;
    }
  }

  :deep(.elem-tpl-block) {
    margin: 8px 0;
    padding: 10px 14px;
    background: #fffbe6;
    border: 1.5px dashed #faad14;
    border-radius: 6px;
    text-align: center;

    .elem-tpl-tag {
      font-size: 1em;
      padding: 2px 10px;
    }
  }

  // 内联图片
  :deep(.doc-inline-image) {
    max-width: 100%;
    height: auto;
    display: block;
    margin: 8px auto;
  }

  // 表格
  :deep(.doc-table) {
    width: 100%;
    border-collapse: separate;
    border-spacing: 0;
    margin: 16px 0;
    font-size: 10.5pt;
    border: 1px solid #d0d5dd;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);

    .doc-table-cell {
      border: 1px solid #e4e7ec;
      border-top: none;
      border-left: none;
      padding: 10px 14px;
      vertical-align: middle;
      line-height: 1.6;
      color: #344054;
      transition: background 0.15s;
    }

    // 右边缘单元格去掉右边框
    tr .doc-table-cell:last-child {
      border-right: none;
    }

    // 表头行
    tr:first-child .doc-table-cell {
      font-weight: 700;
      font-size: 10pt;
      background: linear-gradient(180deg, #f8fafc 0%, #edf2f7 100%);
      color: #1a202c;
      border-bottom: 2px solid #cbd5e1;
      letter-spacing: 0.5px;
      text-align: center;
      padding: 12px 14px;
    }

    // 斑马纹
    tr:not(:first-child):nth-child(even) .doc-table-cell {
      background: #f9fafb;
    }

    tr:not(:first-child):nth-child(odd) .doc-table-cell {
      background: #fff;
    }

    // 行悬停
    tr:not(:first-child):hover .doc-table-cell {
      background: #e8f4ff;
    }
  }

  // 高亮包裹内的表格特殊处理
  :deep(.elem-highlight-wrap .doc-table) {
    margin: 0;
    box-shadow: none;
    border-color: transparent;
  }

  // 空状态 & 加载
  .doc-empty, .doc-loading {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 80px 20px;
    width: 100%;
    max-width: 820px;
    background: var(--white);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
  }

  .doc-loading {
    flex-direction: row;
    gap: 12px;
    padding: 40px;
    font-size: 14px;
    color: var(--text-2);
  }
}

// ==========================================
// 要素高亮弹出框
// ==========================================
.element-popover {
  position: absolute;
  z-index: 1000;
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
  width: 400px;
  overflow: hidden;

  .popover-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 14px;
    background: var(--bg);
    border-bottom: 1px solid var(--border);

    .popover-label {
      font-weight: 600;
      font-size: 14px;
      color: var(--text-1);
    }
  }

  .popover-body {
    padding: 12px 14px;

    .popover-field {
      margin-bottom: 10px;

      &:last-child {
        margin-bottom: 0;
      }

      .popover-field-label {
        display: block;
        font-size: 12px;
        color: var(--text-3);
        margin-bottom: 4px;
      }

      .popover-original {
        font-size: 13px;
        color: var(--text-2);
        background: var(--bg);
        padding: 4px 8px;
        border-radius: var(--radius-sm);
        display: inline-block;
      }
    }
  }

  .popover-rules {
    margin-top: 10px;
    border-top: 1px dashed var(--border);
    padding-top: 10px;

    .rule-trace-card {
      display: flex;
      align-items: flex-start;
      gap: 8px;
      padding: 8px 10px;
      background: var(--bg);
      border-radius: 8px;
      border: 1.5px solid var(--border);
      margin-bottom: 6px;
      cursor: pointer;
      transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);

      &:last-child { margin-bottom: 0; }
      
      &:hover {
        background: linear-gradient(135deg, #f8fbff 0%, #f0f7ff 100%);
        border-color: #409eff;
        transform: translateX(2px);
        box-shadow: 0 2px 8px rgba(64, 158, 255, 0.08);
      }

      .rule-trace-action {
        flex-shrink: 0;
        font-size: 10px;
        font-weight: 600;
        padding: 2px 6px;
        border-radius: 10px;
        line-height: 18px;

        &.action-quote         { background: #e6f4ff; color: #1677ff; }
        &.action-summary       { background: #f6ffed; color: #52c41a; }
        &.action-ai_extract    { background: #e6fffb; color: #13c2c2; }
        &.action-table_extract { background: #fff7e6; color: #fa8c16; }
        &.action-use_entity_value { background: #f0f0f0; color: #666; }
      }

      .rule-trace-info {
        flex: 1;
        min-width: 0;

        .rule-trace-name {
          font-size: 12px;
          color: var(--text-1);
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
        }

        .rule-trace-sources {
          margin-top: 3px;
          display: flex;
          flex-direction: column;
          gap: 2px;

          .rule-trace-att {
            font-size: 11px;
            color: var(--text-3);
            word-break: break-all;
            
            &.clickable {
              cursor: pointer;
              color: var(--el-color-primary);
              &:hover {
                text-decoration: underline;
              }
            }
          }
        }

        .rule-trace-source-text {
          margin-top: 4px;
          padding: 6px 8px;
          background: #fffbe6;
          border-left: 3px solid #faad14;
          border-radius: 2px;
          
          .source-text-label {
            font-size: 10px;
            color: #d48806;
            font-weight: 500;
            display: block;
            margin-bottom: 2px;
          }
          
          .source-text-content {
            font-size: 12px;
            color: #614700;
            line-height: 1.5;
            display: -webkit-box;
            -webkit-line-clamp: 4;
            line-clamp: 4;
            -webkit-box-orient: vertical;
            overflow: hidden;
            word-break: break-all;
          }
        }

        .rule-trace-excerpt {
          margin-top: 4px;
          padding: 4px 6px;
          background: #fafafa;
          border-left: 2px solid #d9d9d9;
          border-radius: 2px;

          .rule-trace-excerpt-text {
            font-size: 11px;
            color: var(--text-2);
            line-height: 1.5;
            display: -webkit-box;
            -webkit-line-clamp: 3;
            -webkit-box-orient: vertical;
            overflow: hidden;
            word-break: break-all;
          }
        }
      }
    }
  }

  .popover-footer {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    padding: 8px 14px;
    border-top: 1px solid var(--border);
    background: var(--bg);
  }
}

// 可编辑文档纸张的光标和选区样式
.doc-paper[contenteditable="true"] {
  outline: none;
  cursor: text;

  &:focus {
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(24, 144, 255, 0.2);
  }
}

// 高亮边框样式
.elem-highlight {
  transition: border-color 0.2s, box-shadow 0.2s;

  &:hover {
    box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.4);
    border-color: #1890ff !important;
  }
}

.elem-highlight-long {
  display: inline !important;
  transition: border-color 0.2s, box-shadow 0.2s;

  &:hover {
    box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.4);
    border-color: #1890ff !important;
  }
}

.elem-highlight-wrap {
  position: relative;
  transition: border-color 0.2s, box-shadow 0.2s;

  &:hover {
    border-color: #1890ff !important;
    box-shadow: 0 0 0 1px rgba(24, 144, 255, 0.4);
  }

  .doc-table {
    margin: 0 auto;
  }
}

// 静态要素淡色高亮
.elem-highlight-static {
  opacity: 0.6;
  transition: opacity 0.2s, border-color 0.2s;

  &:hover {
    opacity: 1;
    border-color: #999 !important;
    box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
  }
}

</style>

<style lang="scss">
// ==========================================
// 点击要素时的闪烁效果（非 scoped，应用于动态生成的 HTML）
// ==========================================
.elem-flash {
  animation: elem-flash-anim 0.4s ease-in-out 3 !important;
  position: relative;
}

@keyframes elem-flash-anim {
  0%, 100% {
    outline: 3px solid rgba(24, 144, 255, 0.4);
    outline-offset: 2px;
    background-color: rgba(24, 144, 255, 0.1) !important;
  }
  50% {
    outline: 5px solid rgba(24, 144, 255, 0.9);
    outline-offset: 4px;
    background-color: rgba(24, 144, 255, 0.25) !important;
  }
}

// ==========================================
// 附件/规则居中悬浮弹窗样式
// ==========================================
.floating-panel-dialog {
  .el-dialog__header {
    padding: 16px 20px 12px;
    border-bottom: 1px solid var(--border);
    margin-right: 0;

    .el-dialog__title {
      font-size: 15px;
      font-weight: 700;
    }
  }

  .el-dialog__body {
    padding: 0;
    max-height: 60vh;
    overflow-y: auto;
  }

  .fp-toolbar {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 20px;
    border-bottom: 1px solid var(--border);
    background: #fafbfc;
    flex-wrap: wrap;

    .fp-count {
      margin-left: auto;
      font-size: 12px;
      color: #999;
      line-height: 1;
    }
  }

  .fp-list {
    padding: 12px 16px 14px;
  }

  &.rule-manage-dialog .el-dialog__body {
    max-height: 72vh;
    display: flex;
    flex-direction: column;
  }

  &.rule-manage-dialog .fp-list {
    max-height: none;
    flex: 1;
    overflow-y: auto;
  }

  // ---- 附件项 ----
  .fp-att-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-radius: 8px;
    cursor: pointer;
    transition: background 0.15s;

    &:hover {
      background: #f5f7fa;
    }

    &.active {
      background: #e8f4ff;
    }

    .att-icon {
      width: 36px;
      height: 36px;
      border-radius: 6px;
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      font-size: 11px;
      font-weight: 700;
      color: #fff;
      background: #1890ff;

      &.pdf { background: #ff4d4f; }
      &.doc, &.docx { background: #1890ff; }
      &.xls, &.xlsx { background: #52c41a; }
      &.img { background: #722ed1; }
      &.zip { background: #fa8c16; }
    }

    .att-info {
      flex: 1;
      min-width: 0;

      .att-name {
        font-size: 13px;
        font-weight: 500;
        color: #1f2937;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .att-meta {
        display: flex;
        gap: 8px;
        margin-top: 2px;
        font-size: 11px;
        color: #999;
      }
    }
  }

  // ---- 解析结果弹窗 ----
  &.parse-result-dialog .el-dialog__body {
    max-height: 85vh;
  }

  .parse-result-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 20px;
    border-bottom: 1px solid var(--border);
    background: #fafbfc;

    .parse-result-info {
      font-size: 12px;
      color: #999;
    }

    .parse-result-actions {
      display: flex;
      gap: 6px;
    }
  }

  .parse-result-content {
    padding: 16px 20px;
    max-height: calc(85vh - 120px);
    overflow-y: auto;

    .parse-result-pre {
      margin: 0;
      padding: 0;
      font-family: 'SF Mono', 'Consolas', 'Monaco', 'Menlo', monospace;
      font-size: 13px;
      line-height: 1.7;
      color: #1f2937;
      white-space: pre-wrap;
      word-wrap: break-word;
    }

    .parse-result-rendered {
      font-size: 14px;
      line-height: 1.8;
      color: #1f2937;

      h1 { font-size: 20px; font-weight: 700; margin: 20px 0 12px; padding-bottom: 6px; border-bottom: 1px solid #eee; }
      h2 { font-size: 18px; font-weight: 700; margin: 18px 0 10px; }
      h3 { font-size: 16px; font-weight: 600; margin: 14px 0 8px; }
      h4, h5, h6 { font-size: 14px; font-weight: 600; margin: 10px 0 6px; }

      p { margin: 8px 0; text-align: justify; }

      img {
        max-width: 100%;
        height: auto;
        display: block;
        margin: 12px auto;
        border-radius: 6px;
        box-shadow: 0 2px 8px rgba(0,0,0,0.1);
      }

      table {
        width: 100%;
        border-collapse: separate;
        border-spacing: 0;
        margin: 12px 0;
        border: 1px solid #d0d5dd;
        border-radius: 6px;
        overflow: hidden;
        font-size: 13px;

        th, td {
          border: 1px solid #e4e7ec;
          padding: 8px 12px;
          vertical-align: top;
          line-height: 1.5;
        }

        th {
          background: linear-gradient(180deg, #f8fafc, #edf2f7);
          font-weight: 600;
          text-align: center;
        }

        tr:nth-child(even) td { background: #f9fafb; }
        tr:hover td { background: #e8f4ff; }
      }

      ol, ul { padding-left: 24px; margin: 8px 0; }
      li { margin: 4px 0; }

      blockquote {
        margin: 12px 0;
        padding: 10px 16px;
        border-left: 4px solid #1890ff;
        background: #f0f7ff;
        color: #374151;
      }

      // 来源文本高亮样式
      mark.source-highlight {
        background: linear-gradient(180deg, #fff3cd, #ffeeba);
        color: #856404;
        padding: 2px 4px;
        border-radius: 3px;
        box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        animation: highlight-pulse 2s ease-in-out;
      }
      
      @keyframes highlight-pulse {
        0%, 100% { background: linear-gradient(180deg, #fff3cd, #ffeeba); }
        50% { background: linear-gradient(180deg, #ffe066, #ffc107); }
      }
      
      /* 溯源定位时的闪烁效果 */
      mark.source-highlight.highlight-flash {
        animation: highlight-flash 0.5s ease-in-out 3;
        outline: 2px solid #f59e0b;
        outline-offset: 2px;
      }
      
      @keyframes highlight-flash {
        0%, 100% { background: linear-gradient(180deg, #ffc107, #ff9800); }
        50% { background: linear-gradient(180deg, #fff3cd, #ffeeba); }
      }
      
      /* 表格行高亮样式（用于评审代码定位） */
      mark.source-highlight-row {
        display: contents;
      }
      mark.source-highlight-row td {
        background: linear-gradient(180deg, #fff3cd, #ffeeba) !important;
        border-left: 3px solid #f59e0b !important;
      }
      tr:has(mark.source-highlight-row) {
        outline: 2px solid #f59e0b;
        outline-offset: -1px;
      }

      code {
        background: #f3f4f6;
        padding: 1px 4px;
        border-radius: 3px;
        font-size: 0.9em;
        font-family: 'SF Mono', 'Consolas', monospace;
      }

      pre {
        background: #1f2937;
        color: #e5e7eb;
        padding: 14px 18px;
        border-radius: 6px;
        overflow-x: auto;
        margin: 12px 0;

        code {
          background: none;
          padding: 0;
          color: inherit;
        }
      }

      hr { border: none; border-top: 1px solid #e5e7eb; margin: 16px 0; }
    }
  }

  // ---- 引用模式提示条 ----
  .citation-mode-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 16px;
    background: linear-gradient(90deg, #e8f5e9, #f1f8e9);
    border-bottom: 1px solid #c8e6c9;
    font-size: 13px;
    color: #2e7d32;
  }

  // ---- 浮动引用工具栏 ----
  .citation-toolbar {
    position: absolute;
    z-index: 3000;
    background: #fff;
    border: 1px solid #d0d5dd;
    border-radius: 10px;
    box-shadow: 0 6px 20px rgba(0,0,0,0.15);
    padding: 10px 14px;
    min-width: 240px;
    max-width: 320px;

    .citation-toolbar-title {
      font-size: 12px;
      color: #666;
      margin-bottom: 8px;
      font-weight: 500;
    }

    .citation-toolbar-actions {
      display: flex;
      gap: 6px;
      flex-wrap: wrap;
    }

    .citation-toolbar-elements {
      max-height: 200px;
      overflow-y: auto;
    }

    .citation-element-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 6px 8px;
      border-radius: 6px;
      cursor: pointer;
      transition: background 0.15s;
      font-size: 13px;

      &:hover {
        background: #f0f7ff;
      }

      .citation-elem-name {
        flex: 1;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        color: #1f2937;
      }
    }
  }

  // ---- 规则引擎预览 ----
  .rule-engine-preview {
    .rule-engine-desc {
      color: #666;
      font-size: 13px;
      margin-bottom: 12px;
    }
    .rule-engine-stats {
      margin-bottom: 16px;
      font-size: 14px;
      strong { color: #409eff; }
    }
    .rule-engine-list {
      max-height: 500px;
      overflow-y: auto;
    }
    .rule-engine-item {
      padding: 12px;
      margin-bottom: 12px;
      background: #f9fafb;
      border-radius: 8px;
      border: 1px solid #e5e7eb;
      
      .rule-engine-header {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-bottom: 8px;
        .rule-engine-name {
          font-weight: 600;
          font-size: 14px;
        }
      }
      .rule-engine-element, .rule-engine-inputs, .rule-engine-source, .rule-engine-code, .rule-engine-desc-text {
        font-size: 13px;
        margin-top: 6px;
        .label {
          color: #666;
          margin-right: 4px;
        }
        code {
          background: #e5e7eb;
          padding: 2px 6px;
          border-radius: 4px;
          font-size: 12px;
        }
      }
      .input-list {
        margin-top: 4px;
        padding-left: 12px;
        .input-item {
          margin-bottom: 6px;
          .input-source { color: #409eff; }
          .input-entry { color: #666; margin-left: 4px; }
          .input-text {
            margin-top: 4px;
            padding: 6px 8px;
            background: #fff;
            border-radius: 4px;
            border: 1px solid #e5e7eb;
            .text-label { color: #999; font-size: 12px; }
            .text-content { color: #333; font-size: 12px; display: block; margin-top: 2px; }
          }
        }
      }
      .source-text {
        color: #333;
        background: #fff;
        padding: 4px 8px;
        border-radius: 4px;
        display: inline-block;
      }
    }
    .rule-engine-json {
      margin-top: 16px;
      .json-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 8px;
        .json-title {
          font-weight: 600;
          font-size: 13px;
          color: #333;
        }
      }
      .json-content {
        background: #1e1e1e;
        color: #d4d4d4;
        padding: 12px;
        border-radius: 6px;
        font-size: 12px;
        font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
        max-height: 300px;
        overflow: auto;
        white-space: pre-wrap;
        word-break: break-all;
      }
    }
  }

  // ---- 规则筛选栏 ----
  .rule-filter-bar {
    display: flex;
    gap: 8px;
    padding: 8px 16px 12px;
    border-bottom: 1px solid #f0f0f0;
    flex-wrap: wrap;

    .rule-filter-tab {
      font-size: 12px;
      padding: 5px 12px;
      border-radius: 14px;
      cursor: pointer;
      color: #666;
      background: #f5f7fa;
      transition: all 0.2s;
      user-select: none;

      em {
        font-style: normal;
        font-size: 11px;
        opacity: 0.7;
        margin-left: 2px;
      }

      &:hover { background: #e8eaed; }
      &.active { background: #409eff; color: #fff; }
      &.active em { opacity: 0.9; }

      &.tab-summary.active        { background: #52c41a; }
      &.tab-ai_extract.active     { background: #13c2c2; }
      &.tab-table_extract.active  { background: #fa8c16; }
      &.tab-quote.active          { background: #1677ff; }
      &.tab-use_entity_value.active { background: #8c8c8c; }
    }
  }

  // ---- 规则项 ----
  .fp-rule-item {
    border-radius: 12px;
    border: 1.5px solid #e8ecf0;
    background: #fff;
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    cursor: pointer;
    margin-bottom: 12px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);

    &:hover { 
      background: linear-gradient(135deg, #fafbfc 0%, #f8fafc 100%);
      border-color: #d9e2ec;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
      transform: translateY(-1px);
    }
    
    &.expanded { 
      background: #fafbfc;
      border-color: #d0dae6;
      box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
    }

    .rule-item-main {
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 14px 16px;
    }

    .rule-action-badge {
      flex-shrink: 0;
      font-size: 11px;
      font-weight: 700;
      padding: 5px 12px;
      border-radius: 16px;
      line-height: 18px;
      white-space: nowrap;
      text-transform: uppercase;
      letter-spacing: 0.3px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);

      &.action-quote           { background: linear-gradient(135deg, #e6f4ff 0%, #d9ecff 100%); color: #1677ff; }
      &.action-summary         { background: linear-gradient(135deg, #f6ffed 0%, #e8f9e0 100%); color: #52c41a; }
      &.action-ai_extract      { background: linear-gradient(135deg, #e6fffb 0%, #d9f7f2 100%); color: #13c2c2; }
      &.action-table_extract   { background: linear-gradient(135deg, #fff7e6 0%, #ffefd9 100%); color: #fa8c16; }
      &.action-use_entity_value { background: linear-gradient(135deg, #f5f5f5 0%, #ececec 100%); color: #666; }
    }

    .rule-info {
      flex: 1;
      min-width: 0;

      .rule-name-row {
        display: flex;
        align-items: center;
        gap: 6px;
        flex-wrap: wrap;

        .rule-name {
          font-size: 14px;
          font-weight: 500;
          color: #1f2937;
        }

        .rule-elem-key {
          font-size: 10px;
          max-width: 200px;
          overflow: hidden;
          text-overflow: ellipsis;
        }
      }

      .rule-desc {
        font-size: 12px;
        line-height: 1.5;
        color: #999;
        margin-top: 4px;
        overflow: hidden;
        text-overflow: ellipsis;
        display: -webkit-box;
        line-clamp: 2;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
      }
    }

    .rule-actions {
      display: flex;
      align-items: center;
      gap: 6px;
      flex-shrink: 0;
    }

    // 展开详情区域
    .rule-detail {
      padding: 2px 14px 12px;
      border-top: 1px dashed #e8e8e8;
      margin: 0 12px 4px;

      .rule-detail-row {
        display: flex;
        align-items: flex-start;
        gap: 8px;
        padding: 6px 0;
        font-size: 12px;

        &:not(:last-child) {
          border-bottom: 1px solid #f5f5f5;
        }
      }

      .rule-detail-label {
        flex-shrink: 0;
        font-weight: 600;
        color: #666;
        width: 60px;
        text-align: right;
      }

      .rule-detail-value {
        color: #333;
        line-height: 1.5;
        word-break: break-all;
      }

      .rule-detail-inputs {
        display: flex;
        flex-wrap: wrap;
        gap: 4px;
      }

      .rule-input-chip {
        font-size: 11px;
        padding: 2px 8px;
        background: #f0f5ff;
        border-radius: 10px;
        color: #1677ff;
        white-space: nowrap;
      }

      .rule-detail-time {
        font-size: 11px;
        color: #999;
        margin-left: 4px;
      }

      .rule-detail-error {
        color: #ff4d4f;
        line-height: 1.4;
      }
    }
  }
}

// 规则工作流弹窗样式 - 完全隐藏 header
.rule-workflow-dialog {
  &.el-dialog .el-dialog__header,
  .el-dialog__header,
  > .el-dialog__header,
  > header {
    display: none !important;
  }
  
  &.el-dialog .el-dialog__body,
  .el-dialog__body,
  > .el-dialog__body {
    padding: 0 !important;
    height: 100vh !important;
    overflow: hidden !important;
  }
}
</style>
