Files
aue/docs/superpowers/plans/2026-05-14-generatepanel-layout-refactoring.md
2026-06-03 13:16:30 +08:00

13 KiB
Raw Permalink Blame History

GeneratePanel 布局重构实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 重构GeneratePanel组件为顶底主从式布局解决内容截断问题并优化用户体验

Architecture: 采用Flexbox垂直布局配置面板flex-shrink: 0固定在顶部结果展示区flex: 1自适应填充剩余空间并独立滚动。表单内部使用CSS Grid双列布局提高信息密度。

Tech Stack: Vue 3 (Composition API), CSS Grid, Flexbox, Scoped Styles


文件结构

需要修改的文件:

  • Modify: src/components/exam/GeneratePanel.vue - 主要重构目标
    • Template部分重新组织DOM结构分离配置区和结果区
    • Script部分无需改动保持现有逻辑
    • Style部分完全重写布局样式

Task 1: 重构GeneratePanel容器布局 - 实现顶底主从式结构

Files:

  • Modify: src/components/exam/GeneratePanel.vue (Template + Style)

  • Step 1: 重新组织Template结构

将现有的两个ContentCard包裹在语义化的容器中

<template>
  <div class="generate-panel">
    <!-- 页面头部和统计保持不变 -->
    <PageHeader
      title="AI智能出题系统"
      description="基于AI技术自动生成高质量试题"
    />
    <StatsRow :stats="examTypeStats" />

    <!-- 新增配置面板容器 -->
    <div class="config-panel">
      <ContentCard title="智能出题配置">
        <template #header>
          <h2 class="card-title">
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
              <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
              <line x1="8" y1="21" x2="16" y2="21"></line>
              <line x1="12" y1="17" x2="12" y2="21"></line>
            </svg>
            智能出题配置
          </h2>
        </template>
        
        <!-- 表单内容将在Task 2中优化 -->
        <div class="form-content">
          <!-- 现有表单代码保持不变 -->
          <div class="form-item">...</div>
          <div class="form-item">...</div>
          <div class="form-item">...</div>
          <div class="action-bar">...</div>
        </div>
      </ContentCard>
    </div>

    <!-- 新增结果展示区容器 -->
    <div class="result-panel">
      <ContentCard title="待审核题目列表">
        <div v-if="isGenerating" class="loading-state">...</div>
        
        <div v-else-if="generatedQuestions.length === 0" class="empty-state">...</div>
        
        <div v-else class="question-list">
          <!-- 现有题目列表代码保持不变 -->
        </div>
      </ContentCard>
    </div>
  </div>
</template>
  • Step 2: 重写容器级CSS样式
.generate-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  padding: 16px;
  overflow: hidden; /* 外层不滚动 */
}

/* 配置面板 - 固定高度,不压缩 */
.config-panel {
  flex-shrink: 0;
  margin-bottom: 12px;
}

/* 结果展示区 - 自适应填充剩余空间 */
.result-panel {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.result-panel :deep(.content-card) {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.result-panel :deep(.card-body) {
  flex: 1;
  overflow-y: auto; /* 内容独立滚动 */
}
  • Step 3: 验证基础布局结构

刷新浏览器,确认:

  • 配置面板显示在顶部且完整可见
  • 结果展示区占据下方所有剩余空间
  • 整体无滚动条(除了结果区的内部滚动)

Task 2: 优化配置表单为双列Grid布局 - 提高信息密度

Files:

  • Modify: src/components/exam/GeneratePanel.vue (Template中的.form-content部分)

  • Step 1: 将表单项重组为Grid布局

替换.form-content内部的HTML结构

<div class="form-content">
  <!-- 文件选择器 - 跨两列 -->
  <div class="form-item form-item--full">
    <label class="form-label">选择制度文件 <span class="required-mark">*</span></label>
    <FileSelector 
      v-model="selectedDoc" 
      @change="onDocSelected" 
      @collection-change="onCollectionChange"
      placeholder="先选择知识库,再选择制度文件..." 
    />
    <div v-if="selectedCollectionName" class="collection-info">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
        <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
        <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
      </svg>
      <span>当前知识库: <strong>{{ selectedCollectionName }}</strong></span>
    </div>
    <div v-if="selectedDocData" class="selected-file-info">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
        <polyline points="14 2 14 8 20 8"></polyline>
      </svg>
      <span>{{ selectedDocData.title || selectedDocData.rawFileName }}</span>
      <span>v{{ selectedDocData.version }}</span>
    </div>
  </div>

  <!-- 难度等级 + 总计题数 - 并排显示 -->
  <div class="form-item">
    <label class="form-label">难度等级</label>
    <select v-model="difficulty" class="form-select">
      <option :value="1">简单</option>
      <option :value="2">中等</option>
      <option :value="3">困难</option>
    </select>
  </div>

  <div class="form-item form-item--total">
    <label class="form-label">总计题数</label>
    <div class="total-display">
      <strong>{{ totalQuestionCount }}</strong> 
    </div>
  </div>

  <!-- 题型与数量配置 - 跨两列 -->
  <div class="form-item form-item--full">
    <label class="form-label">题型与数量配置</label>
    <div class="type-count-grid">
      <div class="type-count-item" v-for="qt in questionTypeOptions" :key="qt.value">
        <label class="checkbox-label" :class="{ active: questionTypeCounts[qt.value] > 0 }">
          <input type="checkbox" :checked="questionTypeCounts[qt.value] > 0" @change="toggleQuestionType(qt.value)" />
          {{ qt.label }}
        </label>
        <input 
          type="number" 
          v-model.number="questionTypeCounts[qt.value]" 
          min="0" 
          max="50" 
          class="form-input-sm"
          :disabled="questionTypeCounts[qt.value] <= 0"
          placeholder="0"
        />
      </div>
    </div>
  </div>

  <!-- 操作按钮 - 跨两列右对齐 -->
  <div class="action-bar">
    <button class="btn btn-primary" @click="handleGenerateQuestions" :disabled="isGenerating">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
        <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"></path>
        <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"></path>
      </svg>
      {{ isGenerating ? '生成中...' : '开始生成题目' }}
    </button>
    <button class="btn btn-secondary" @click="generatedQuestions = []">
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
        <polyline points="3 6 5 6 21 6"></polyline>
        <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
      </svg>
      清空结果
    </button>
  </div>
</div>
  • Step 2: 编写Grid布局CSS
.form-content {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px 16px;
  align-items: start;
}

/* 跨列元素 */
.form-item--full {
  grid-column: 1 / -1;
}

/* 难度等级和总计的特殊样式 */
.form-item--total .total-display {
  padding: 6px 12px;
  background: #F8F9FA;
  border: 1px solid #DEE2E6;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 600;
  color: #212529;
  text-align: center;
}

/* 按钮栏右对齐 */
.action-bar {
  justify-content: flex-end;
  padding-top: 12px;
  border-top: 1px solid #E9ECEF;
  margin-top: 4px;
}

/* 题型网格优化为3列 */
.type-count-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin-top: 6px;
}
  • Step 3: 验证双列布局效果

确认:

  • 文件选择器独占一行跨2列
  • 难度等级和总计题数左右并排
  • 题型配置以3列网格显示
  • 操作按钮右对齐且跨2列

Task 3: 实现结果展示区独立滚动 - 自适应高度填充

Files:

  • Modify: src/components/exam/GeneratePanel.vue (Style部分)

  • Step 1: 优化空状态和加载状态样式

.loading-state,
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 32px 16px;
  color: #6C757D;
  min-height: 150px;
}

.loading-icon,
.empty-icon {
  font-size: 36px;
  margin-bottom: 10px;
}

.empty-state p {
  font-size: 14px;
  font-weight: 500;
  color: #495057;
  margin-bottom: 4px;
}

.empty-hint {
  font-size: 13px;
  color: #6C757D;
  margin-top: 6px;
  max-width: 400px;
  text-align: center;
  line-height: 1.5;
}
  • Step 2: 优化题目列表样式
.question-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 4px;
}

.question-item {
  padding: 14px;
  background: #FFFFFF;
  border: 1px solid #E9ECEF;
  border-radius: 8px;
  transition: box-shadow 0.15s ease, transform 0.15s ease;
}

.question-item:hover {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  transform: translateY(-1px);
}
  • Step 3: 测试滚动行为

生成一些测试数据或使用loading状态验证

  • 结果区可以独立滚动
  • 配置面板保持固定不动
  • 无整体页面滚动条
  • 滚动条样式美观(可选优化)

Task 4: 响应式适配与细节优化 - 移动端降级处理

Files:

  • Modify: src/components/exam/GeneratePanel.vue (Style部分添加媒体查询)

  • Step 1: 添加移动端响应式规则

/* 平板设备 (≤1024px) */
@media (max-width: 1024px) {
  .type-count-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  
  .form-content {
    gap: 10px 12px;
  }
}

/* 移动设备 (≤768px) */
@media (max-width: 768px) {
  .generate-panel {
    padding: 12px;
  }
  
  /* 降级为单列布局 */
  .form-content {
    grid-template-columns: 1fr;
    gap: 10px;
  }
  
  .form-item--full,
  .action-bar {
    grid-column: 1;
  }
  
  /* 题型降为2列 */
  .type-count-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  
  /* 按钮全宽 */
  .action-bar {
    flex-direction: column;
  }
  
  .action-bar .btn {
    width: 100%;
  }
  
  /* 减少间距 */
  .config-panel {
    margin-bottom: 8px;
  }
  
  /* 缩小字体和内边距 */
  .form-label {
    font-size: 12px;
  }
  
  .type-count-item {
    padding: 6px 8px;
  }
}

/* 小屏手机 (≤480px) */
@media (max-width: 480px) {
  .type-count-grid {
    grid-template-columns: 1fr;
  }
  
  .stats-row {
    font-size: 12px;
  }
}
  • Step 2: 测试不同屏幕尺寸

使用浏览器开发者工具测试:

  • 桌面端 (1920x1080): 双列布局完美呈现

  • 笔记本 (1366x768): 正常显示

  • 平板 (768x1024): 降级为单列,按钮堆叠

  • 手机 (375x667): 单列紧凑布局

  • Step 3: 最终视觉检查

确认所有细节:

  • 无内容溢出或被截断
  • 所有交互元素可点击
  • 样式符合项目白色体系规范
  • 过渡动画流畅自然
  • 空状态、加载状态、正常状态均正确显示

验收标准

完成所有任务后GeneratePanel应满足以下要求

  1. 布局正确性

    • 配置面板始终可见,不被遮挡
    • 结果区自适应填充剩余空间
    • 仅结果区支持独立滚动
  2. 表单可用性

    • 双列Grid布局信息密度合理
    • 所有表单项完整显示
    • 操作按钮易于触达
  3. 响应式表现

    • 桌面端:双列布局
    • 移动端:自动降级为单列
    • 无横向滚动条
  4. 视觉一致性

    • 符合项目白色体系设计规范
    • 与其他模块风格统一
    • 间距、字号、圆角等细节一致

执行选项

Plan complete and saved to this document. Two execution options:

1. Subagent-Driven (recommended) - I dispatch a fresh subagent per task, review between tasks, fast iteration

2. Inline Execution - Execute tasks in this session using executing-plans, batch execution with checkpoints

Which approach?