cursor-vue3表单skills

我爱海鲸 2026-06-02 15:09:35 暂无标签

简介vue3表单skills、导出

可以把技能放到本机用户目录的 ~/.cursor/skills/<技能目录>/(Windows 一般是 C:\Users\你的用户名\.cursor\skills\)。这样任意项目打开后,只要 Cursor 会加载用户级 skills,就不必每个仓库复制一份。


注意:~/.cursor/skills-cursor/ 是 Cursor 内置技能目录,不要把自己的技能放进去;自定义技能用 项目 .cursor/skills/ 或 用户 ~/.cursor/skills/。

在项目的根目录下创建:.cursor\skills\admin-table-page

SKILL.md

---
name: admin-table-page
description: Scaffolds Vue 3 + Element Plus admin list pages with inline filter form, fixed-layout scrollable table, pagination, optional Excel export, column visibility/order (localStorage + sortablejs), and fixed-right actions column. Resolves API paths and types from the workspace. Use when adding 后台列表页, 筛选表单, 表格分页, Excel 导出, 列显示, 列拖拽, or cms admin table views.
---

# 后台「筛选 + 表格 + 分页」列表页(Vue 3 + Element Plus)

在 **cms** 模块新增或与现有页面对齐的**管理端列表页**时,按下列约定生成代码。**技能不含具体业务字段、接口路径、枚举文案**;落地前须从工作区打开同类页面与 `src/api`、`src/types` 复制命名与路径。

## 0. 先读再写

| 要确定的内容 | 做法 |
|-------------|------|
| **页面模板** | 打开 `cms/src/views` 下已有列表页(如带导出、列设置的页面),复制 Layout / 卡片 / 表格区结构。 |
| **API 模块** | 打开 `cms/src/api` 下同域 `*.ts`:`get` 分页、`request.get` + `responseType: 'blob'` 导出。 |
| **类型** | 在 `cms/src/types` 对照 `*QueryParams`、`MpPage`、行 `Item` 定义。 |
| **请求工具** | 从 `@/utils/request` 复制 `get` / 默认 `request` 用法;时间格式与后端 `yyyy-MM-dd HH:mm:ss` 一致。 |
| **表头配色** | 若项目已有统一表头色,复制其 `:header-cell-style` 与 `:deep(th...)`;否则用 Element 默认。 |

更完整的分步骨架见 [examples.md](examples.md)。后端分页/导出配对见 [`spring-admin-page-export`](../spring-admin-page-export/SKILL.md)。

---

## 1. 页面分区(自上而下)

```
Layout
└─ .{page}-page          ← height: calc(100vh - 顶栏与 padding 合计)
   └─ el-card.main-card  ← flex 列,min-height: 0
      ├─ el-form.search-form     ← 筛选,flex-shrink: 0
      ├─ [可选] .summary-bar     ← 汇总/提示条,flex-shrink: 0
      └─ .table-panel            ← flex: 1,min-height: 0
         ├─ .table-wrap (ref)    ← 表格滚动区;overflow: visible(见 §4)
         │  └─ el-table (:height="tableHeight")
         └─ .pager               ← el-pagination,flex-shrink: 0
```

**可选**:`el-dialog` 详情 / 二次确认;行内 `link` 按钮放**固定右侧操作列**。

---

## 2. 筛选表单

- `el-form`:`inline`、`@submit.prevent="onSearch"`。
- `queryForm` 用 `reactive`;字段与后端 Query DTO **驼峰**对齐。
- 常用控件:`el-input`(精确)、`el-select`(单/多选、`clearable`)、`el-date-picker`(`datetimerange`,`value-format="YYYY-MM-DD HH:mm:ss"`)。
- 操作按钮:**查询**(`onSearch` → `pageNum=1` + `fetchList`)、**重置**(清空表单 + 默认分页 + `fetchList`)。
- **导出 Excel**(若后端有 `/export`):`type="success" plain`、`:loading="exporting"`;参数用**无分页**的 `buildExportParams()`(与列表筛选一致,不含 `pageNum`/`pageSize`)。

---

## 3. 列表数据流

| 函数 | 职责 |
|------|------|
| `buildParams()` | 分页 + 当前筛选 → 传给 `page*` API |
| `buildExportParams()` | 仅筛选 → 传给 `export*` / `download*Excel` |
| `fetchList()` | `loading`;更新 `tableData`、`total`;`nextTick` 后 `updateTableHeight()`、`initColumnDrag()`(若启用列拖拽) |
| `onSizeChange()` | `pageNum = 1` 再 `fetchList` |

分页状态:`pageNum`、`pageSize`、`total`;`el-pagination` 绑定 `@current-change="fetchList"`、`@size-change="onSizeChange"`。

---

## 4. 表格布局(固定高度 + 内部滚动)

- 页面根容器 **禁止** 整体纵向滚动;表格区 `flex: 1; min-height: 0`。
- `tableWrapRef` + `ResizeObserver`(及 `window.resize`)计算 `tableHeight = max(wrap.clientHeight, 200)`。
- `el-table` 设 `:height="tableHeight"`;数据变更后 `tableRef.doLayout()`。
- **固定右侧操作列**:`.table-wrap` 与 `.table-panel` 使用 `overflow: visible`,**不要** `overflow: hidden`,否则 `fixed="right"` 会被裁切。

宽表:`.order-table { width: max(100%, Npx) }`,body 区 `overflow-x: auto`。

---

## 5. 列配置(显示 / 顺序 / 拖拽)

适用宽表、字段多的列表;简单列表可写死 `el-table-column`。

1. **配置表** `TABLE_COLUMNS: { key, label }[]` + `tableColumnBindMap`(`prop`、`label`、`width`/`minWidth`、`showOverflowTooltip`)。
2. **可见列** `visibleColumnKeys` + `localStorage`(key 建议:`{页面标识}-visible-columns`);至少保留 1 列。
3. **列顺序** `columnOrder` + 独立 `localStorage`;`orderedVisibleColumnKeys = columnOrder.filter(visible)`。
4. **渲染**:`v-for="colKey in orderedVisibleColumnKeys"` + `v-bind="tableColumnBindMap[colKey]"`;需自定义单元格时用 `#default` + `v-if/v-else-if` 按 `colKey` 分支。
5. **列设置入口**:放在**操作列表头**(小图标 + `el-popover` + `el-checkbox-group`);「恢复默认」重置可见性与顺序。
6. **表头拖拽**:依赖 `sortablejs`;`initColumnDrag` 绑定 `.el-table__header-wrapper thead tr`,`draggable: '.draggable-column-header'`;`onEnd` 更新 `columnOrder` 并 `persist`;`onUnmounted` 销毁实例。列变化后 `nextTick` → `doLayout` → 重新 `initColumnDrag`。

操作列:`fixed="right"`,不参与拖拽;表头 `label-class-name="draggable-column-header"` 仅加在数据列。

---

## 6. API 层(分页 + 导出)

在对应 `src/api/*.ts` 中(命名随模块):

```typescript
// 分页
pageRecords(params?: RecordQueryParams): Promise<MpPage<RecordItem>> {
  return get<MpPage<RecordItem>>('/cms-api/.../page', params)
}

// 导出 blob
exportRecordsExcel(params?: RecordQueryParams): Promise<Blob> {
  return request.get('/cms-api/.../export', {
    params,
    responseType: 'blob',
    timeout: 120_000
  }) as unknown as Promise<Blob>
}

// 触发浏览器下载
async downloadRecordsExcel(params?: RecordQueryParams): Promise<void> {
  const blob = await api.exportRecordsExcel(params)
  // createObjectURL + <a download> + revokeObjectURL
}
```

页面内:`await api.downloadRecordsExcel(buildExportParams())`,成功 `ElMessage.success`,失败 `ElMessage.error`。

---

## 7. 操作列与行内交互

- 操作按钮:`type="primary" link size="small"`,包在 `.table-ops`(纵向 `flex`,居中)。
- 异步行操作:用 `actionLoadingRowId` 等 ref 绑定 `:loading`,`finally` 清空。
- 危险操作:`ElMessageBox.confirm`(必要时两步确认)。
- 详情:`el-dialog` + `destroy-on-close`;`v-loading` 包裹内容区。

---

## 8. 样式检查清单

- [ ] 页面高度固定,筛选/分页不随表格滚动
- [ ] 固定操作列完整可见
- [ ] 表头与表体列宽在列显隐/拖拽后仍正常(`doLayout`)
- [ ] 导出参数与列表筛选一致且无分页字段
- [ ] `localStorage` key 含页面唯一前缀,避免多页冲突
- [ ] 无业务魔法字符串散落:枚举/格式化抽到 `utils` 或 `types`(按项目习惯)

---

## 9. 与后端技能边界

- 本技能:**前端**列表页结构与交互。
- [`spring-admin-page-export`](../spring-admin-page-export/SKILL.md):**后端** `GET /page` + `GET /export` 配对。

新增导出时前后端须共用同一套 Query 筛选语义。

---

## 10. 附加资源

- 结构骨架与常量命名:[examples.md](examples.md)

examples.md:

# 后台列表页:结构骨架(无业务字段)

下列占位符落地时替换:`Record*`、`{pageId}`、`{apiPrefix}`、`/cms-api/...`。包路径、import 从工作区同类文件复制。

---

## 1. 模板骨架

```vue
<template>
  <Layout>
    <div class="{pageId}-page">
      <el-card shadow="never" class="main-card">
        <el-form :inline="true" :model="queryForm" class="search-form" @submit.prevent="onSearch">
          <!-- 筛选表单项:按后端 Query DTO 逐项添加 -->
          <el-form-item>
            <el-button type="primary" @click="onSearch">查询</el-button>
            <el-button @click="onReset">重置</el-button>
            <el-button v-if="hasExport" type="success" plain :loading="exporting" @click="onExport">
              导出 Excel
            </el-button>
          </el-form-item>
        </el-form>

        <!-- 可选汇总条 -->
        <!-- <div class="summary-bar">...</div> -->

        <div class="table-panel">
          <div ref="tableWrapRef" class="table-wrap">
            <el-table
              ref="tableRef"
              v-loading="loading"
              class="data-table"
              :data="tableData"
              :height="tableHeight"
              :header-cell-style="tableHeaderCellStyle"
              border
              stripe
              size="small"
            >
              <el-table-column
                v-for="colKey in orderedVisibleColumnKeys"
                :key="colKey"
                :column-key="colKey"
                label-class-name="draggable-column-header"
                v-bind="tableColumnBindMap[colKey]"
              >
                <!-- 需要格式化/插槽的列按 colKey 分支 -->
              </el-table-column>

              <el-table-column width="W" fixed="right" align="center" class-name="ops-column">
                <template #header>
                  <div class="ops-column-header">
                    <span class="ops-column-title">操作</span>
                    <!-- 列显示 popover(见 SKILL.md §5) -->
                  </div>
                </template>
                <template #default="{ row }">
                  <div class="table-ops">
                    <el-button type="primary" link size="small" @click="openDetail(row)">查看详情</el-button>
                  </div>
                </template>
              </el-table-column>
            </el-table>
          </div>

          <div class="pager">
            <el-pagination
              v-model:current-page="pageNum"
              v-model:page-size="pageSize"
              :total="total"
              :page-sizes="[10, 20, 50, 100]"
              layout="total, sizes, prev, pager, next, jumper"
              background
              @current-change="fetchList"
              @size-change="onSizeChange"
            />
          </div>
        </div>
      </el-card>
    </div>
  </Layout>
</template>
```

---

## 2. Script 核心状态

```typescript
const queryForm = reactive({
  // 与 RecordQueryParams 对齐
  timeRange: null as [string, string] | null
})

const pageNum = ref(1)
const pageSize = ref(10)
const total = ref(0)
const tableData = ref<RecordItem[]>([])
const loading = ref(false)
const exporting = ref(false)

const tableWrapRef = ref<HTMLElement | null>(null)
const tableRef = ref<TableInstance>()
const tableHeight = ref(360)
```

---

## 3. 参数构建

```typescript
function buildParams(): RecordQueryParams {
  const p: RecordQueryParams = {
    pageNum: pageNum.value,
    pageSize: pageSize.value
  }
  // if (queryForm.xxx) p.xxx = queryForm.xxx
  if (queryForm.timeRange?.length === 2) {
    p.createTimeBegin = queryForm.timeRange[0]
    p.createTimeEnd = queryForm.timeRange[1]
  }
  return p
}

function buildExportParams(): RecordQueryParams {
  const p: RecordQueryParams = {}
  // 与 buildParams 相同筛选,不含 pageNum/pageSize
  return p
}
```

---

## 4. 列 localStorage 常量

```typescript
const COLUMN_STORAGE_KEY = '{pageId}-visible-columns'
const COLUMN_ORDER_STORAGE_KEY = '{pageId}-column-order'

type TableColumnKey = 'id' | 'name' /* ... */

const TABLE_COLUMNS: { key: TableColumnKey; label: string }[] = [
  { key: 'id', label: 'ID' }
  // ...
]
```

加载时需过滤无效 key,并 append 配置中新增但 storage 中缺失的列。

---

## 5. 表格高度

```typescript
function updateTableHeight() {
  const el = tableWrapRef.value
  if (!el) return
  tableHeight.value = Math.max(el.clientHeight, 200)
  nextTick(() => tableRef.value?.doLayout?.())
}

let tableResizeObserver: ResizeObserver | null = null

onMounted(async () => {
  await fetchList()
  await nextTick()
  updateTableHeight()
  initColumnDrag()
  if (typeof ResizeObserver !== 'undefined' && tableWrapRef.value) {
    tableResizeObserver = new ResizeObserver(() => updateTableHeight())
    tableResizeObserver.observe(tableWrapRef.value)
  }
  window.addEventListener('resize', updateTableHeight)
})

onUnmounted(() => {
  destroyColumnDrag()
  tableResizeObserver?.disconnect()
  window.removeEventListener('resize', updateTableHeight)
})
```

---

## 6. 表头样式(项目统一色示例)

```typescript
const tableHeaderCellStyle = {
  background: '#f0fdfa',
  color: '#0f766e',
  fontWeight: 600
} as const
```

```scss
.{pageId}-page {
  height: calc(100vh - 100px);
  max-height: calc(100vh - 100px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.main-card {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;

  :deep(.el-card__body) {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
}

.table-panel {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: visible;
}

.table-wrap {
  flex: 1;
  min-height: 0;
  overflow: visible;
  position: relative;
}

.pager {
  flex-shrink: 0;
  margin-top: 12px;
  display: flex;
  justify-content: flex-end;
}
```

---

## 7. 导出

```typescript
async function onExport() {
  exporting.value = true
  try {
    await recordApi.downloadRecordsExcel(buildExportParams())
    ElMessage.success('导出已开始,请查看浏览器下载')
  } catch (e: unknown) {
    const err = e as { msg?: string; message?: string }
    ElMessage.error(err?.msg || err?.message || '导出失败')
  } finally {
    exporting.value = false
  }
}
```

---

## 8. 依赖

列拖拽需 `sortablejs`(及 `@types/sortablejs`)。若 `cms/package.json` 未声明,安装后再引用。

你好:我的2025