# 源数据上传紧凑文件列表 Implementation Plan

> **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:** 将数据处理创建页的已上传文件从大卡片改为固定高度、可滚动且可逐项操作的紧凑列表。

**Architecture:** 只修改 `TaskSetupStep` 的模板和局部样式，继续消费现有的
`uploadedFiles` 属性并派发既有 `remove-file` 事件。使用同一组件内的标题栏和
滚动容器管理信息密度，不改变上传、格式限制或父组件数据流。

**Tech Stack:** Vue 3 `<script setup>`、TypeScript、Element Plus、SCSS、Node.js 回归脚本。

## Global Constraints

- 不新增依赖、组件、数据字段或父子组件事件。
- 上传区、格式限制、校验语义与逐项删除行为必须保持不变。
- 空状态保留大尺寸拖拽上传区与格式提示；存在至少一个文件时，改为列表标题右侧
  的小型“继续上传”入口，且复用相同上传属性与文件变更事件。
- 列表默认显示最多 5 行；额外文件只能在列表内部纵向滚动。
- 文件名允许省略，但必须通过原生 `title` 保留完整文本。
- 校验状态必须同时显示图标和文字；删除按钮必须可见且可键盘操作。
- 当前工作区不是 Git 仓库，本计划不包含提交操作。

---

### Task 1: 紧凑上传文件列表

**Files:**
- Modify: `frontend/scripts/regression-data-process-wizard.mjs`
- Modify: `frontend/src/views/data-process/create/TaskSetupStep.vue:157-164,314-351`

**Interfaces:**
- Consumes: `uploadedFiles: { uid: string | number; name: string; size: number; count: number }[]`
  和 `remove-file(uid)` 事件。
- Produces: `.uploaded-file-list-header`、`.uploaded-file-items` 和每条
  `.uploaded-file` 紧凑行；不新增对外 TypeScript 接口。

- [ ] **Step 1: 添加会失败的页面结构回归断言**

在 `frontend/scripts/regression-data-process-wizard.mjs` 的第二步组件检查之后读取
`TaskSetupStep.vue`，并追加以下断言：

```js
const taskSetupPath = path.join(createDir, 'TaskSetupStep.vue')
const taskSetupSource = await readFile(taskSetupPath, 'utf8')

for (const marker of [
  'uploaded-file-list-header',
  'uploaded-file-items',
  '已添加 {{ uploadedFiles.length }} 个文件',
  ':title="file.name"',
  'aria-label="已上传文件列表"',
  '继续上传',
  'v-if="uploadedFiles.length === 0"',
]) {
  assert.ok(taskSetupSource.includes(marker), `源数据文件列表缺少：${marker}`)
}
assert.match(taskSetupSource, /\.uploaded-file-items\s*\{[\s\S]*max-height:\s*240px/, '文件列表没有固定可见高度')
assert.match(taskSetupSource, /\.uploaded-file-items\s*\{[\s\S]*overflow-y:\s*auto/, '超出文件没有在列表内滚动')
assert.match(taskSetupSource, /compact-upload[\s\S]*:accept="uploadAccept"/, '继续上传没有复用格式限制')
assert.match(taskSetupSource, /compact-upload[\s\S]*on-change/, '继续上传没有复用文件变更事件')
```

- [ ] **Step 2: 运行回归脚本，确认新增断言失败**

Run: `npm --prefix frontend run test:data-process-wizard`

Expected: FAIL，错误信息包含新增文件列表、列表可访问性或继续上传状态切换的缺失标记。

- [ ] **Step 3: 更新模板为带标题与滚动区域的紧凑列表**

在 `TaskSetupStep.vue` 中，将当前上传区改为两种互斥状态：空状态保留大尺寸拖拽
区和格式提示；存在文件时显示含继续上传入口的紧凑列表。两个上传入口均保留
`multiple`、`:accept="uploadAccept"`、`:auto-upload="false"` 和相同 `on-change` 事件。
文件图标、元信息、成功状态和原有删除事件必须保留：

```vue
<el-upload
  v-if="uploadedFiles.length === 0"
  class="upload-empty-state"
  drag
  multiple
  :accept="uploadAccept"
  :auto-upload="false"
  :show-file-list="false"
  :on-change="(file: UploadFile) => emit('file-change', file)"
>
  <!-- 保留现有大尺寸上传引导和格式提示 -->
</el-upload>

<div v-else class="uploaded-file-list" aria-label="已上传文件列表">
  <div class="uploaded-file-list-header">
    <span>已添加 {{ uploadedFiles.length }} 个文件</span>
    <el-upload
      class="compact-upload"
      multiple
      :accept="uploadAccept"
      :auto-upload="false"
      :show-file-list="false"
      :on-change="(file: UploadFile) => emit('file-change', file)"
    >
      <el-button size="small" type="primary">继续上传</el-button>
    </el-upload>
  </div>
  <div class="uploaded-file-items">
    <div v-for="file in uploadedFiles" :key="file.uid" class="uploaded-file">
      <span class="file-icon"><i class="fa fa-file-text-o" /></span>
      <div class="file-main">
        <strong :title="file.name">{{ file.name }}</strong>
        <span>{{ formatSize(file.size) }}<template v-if="file.count"> · {{ file.count.toLocaleString() }} 条</template></span>
      </div>
      <span class="file-status"><i class="fa fa-check-circle" /> 校验通过</span>
      <el-button link type="danger" @click="emit('remove-file', file.uid)">删除</el-button>
    </div>
  </div>
</div>
```

- [ ] **Step 4: 将现有大卡片样式改为紧凑行和内部滚动**

在 `TaskSetupStep.vue` 中替换现有 `.uploaded-file-list` 与 `.uploaded-file` 相关样式，
使容器、标题、滚动区和行高满足以下实现：

```scss
.uploaded-file-list {
  margin-top: 20px;
  overflow: hidden;
  background: #fff;
  border: 1px solid #dfe3ea;
  border-radius: 8px;
}

.uploaded-file-list-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 14px;
  color: #5f6878;
  font-size: 12px;
  background: #fbfcfe;
  border-bottom: 1px solid #edf0f5;
}

.uploaded-file-items {
  max-height: 240px;
  overflow-y: auto;
}

.compact-upload {
  flex: 0 0 auto;
}

.uploaded-file {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 48px;
  padding: 8px 14px;
  border-bottom: 1px solid #edf0f5;

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

.file-icon {
  width: 28px;
  height: 28px;
  font-size: 14px;
  border-radius: 7px;
}

.file-main strong {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
```

保留 `.file-main` 的弹性填充与 `.file-status` 的成功色；为窄屏媒体查询加入更小的
行间距和可换行的状态文本，保证删除按钮不被文件名挤出。

- [ ] **Step 5: 运行回归脚本并执行类型检查**

Run: `npm --prefix frontend run test:data-process-wizard && npm --prefix frontend run type-check`

Expected: 两个命令均以退出码 `0` 完成，前者输出 `数据处理四步向导回归检查通过`。

- [ ] **Step 6: 构建生产包，检查样式与模板编译**

Run: `npm --prefix frontend run build`

Expected: 退出码 `0`，Vite 输出生产构建产物信息，无 Vue 模板或 SCSS 编译错误。

- [ ] **Step 7: 检查变更范围**

Run: `git diff -- frontend/src/views/data-process/create/TaskSetupStep.vue frontend/scripts/regression-data-process-wizard.mjs`

Expected: 当前目录不是 Git 仓库时，该命令会报告仓库缺失；改用
`diff -u <(git show HEAD:...) ...` 不可用，因此使用 `sed` 复查两个文件的目标区段，
确认没有修改上传、格式限制或父组件事件。
