cursor-vue3表单skills

我爱海鲸 2026-06-25 22:35:00 暂无标签

简介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), fixed-right actions column, and CreateTimeRangeFilter (today shortcut for createTime). 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` 一致。 |
| **创建时间筛选** | 凡筛选含 `createTimeBegin` / `createTimeEnd` 的列表,统一用 `@/components/filter/CreateTimeRangeFilter.vue`(含「今天」快捷勾选),勿单独写裸 `el-date-picker`。 |
| **表头配色** | 若项目已有统一表头色,复制其 `: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`)。
- **创建时间**(有 `createTimeBegin` / `createTimeEnd` 的后端分页查询):
  - `queryForm.timeRange: [string, string] | null`;
  - 模板使用 **`CreateTimeRangeFilter`**(`v-model="queryForm.timeRange"`),**禁止**在该场景单独写 `el-date-picker`;
  - `buildParams` / `buildExportParams` / `appendFilterParams` 中统一映射:

    ```typescript
    if (queryForm.timeRange?.length === 2) {
      target.createTimeBegin = queryForm.timeRange[0]
      target.createTimeEnd = queryForm.timeRange[1]
    }
    ```

  - **`onReset`** 须 `queryForm.timeRange = null`(会同步取消「今天」勾选)。
- 操作按钮:**查询**(`onSearch` → `pageNum=1` + `fetchList`)、**重置**(清空表单 + 默认分页 + `fetchList`)。
- **导出 Excel**(若后端有 `/export`):`type="success" plain`、`:loading="exporting"`;参数用**无分页**的 `buildExportParams()`(与列表筛选一致,不含 `pageNum`/`pageSize`)。

### 2.1 创建时间 +「今天」快捷(`CreateTimeRangeFilter`)

**组件路径**:`ad-cms/src/components/filter/CreateTimeRangeFilter.vue`

**工具函数**(`@/utils/datetime.ts`):

| 函数 | 说明 |
|------|------|
| `getTodayDateTimeRange()` | 返回 `[当天 00:00:00, 当天 23:59:59]`,格式 `YYYY-MM-DD HH:mm:ss` |
| `isTodayDateTimeRange(range)` | 判断当前范围是否等于「今天」全天 |

**交互约定**:

| 操作 | 行为 |
|------|------|
| 勾选「今天」 | `timeRange` → 当天 `00:00:00` ~ `23:59:59` |
| 取消「今天」 | `timeRange` → `null` |
| 手动改日期范围 | 若等于今天全天则自动勾选;否则自动取消 |
| 重置表单 | `timeRange = null`,「今天」同步取消 |

**页面接入**:

```vue
<script setup lang="ts">
import CreateTimeRangeFilter from '@/components/filter/CreateTimeRangeFilter.vue'
</script>

<el-form-item label="创建时间">
  <CreateTimeRangeFilter v-model="queryForm.timeRange" />
</el-form-item>
```

组件内部已固定 `datetimerange`、`value-format="YYYY-MM-DD HH:mm:ss"` 与宽度;新列表页**直接复用**,勿复制 date-picker 配置。

---

## 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`)
- [ ] 导出参数与列表筛选一致且无分页字段
- [ ] 含创建时间筛选时使用 `CreateTimeRangeFilter`,且 `onReset` 清空 `timeRange`
- [ ] `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 逐项添加 -->
          <!-- 有 createTimeBegin/createTimeEnd 时: -->
          <!-- <el-form-item label="创建时间">
            <CreateTimeRangeFilter v-model="queryForm.timeRange" />
          </el-form-item> -->
          <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
import CreateTimeRangeFilter from '@/components/filter/CreateTimeRangeFilter.vue'

const queryForm = reactive({
  // 与 RecordQueryParams 对齐
  timeRange: null as [string, string] | null, // 映射 createTimeBegin / createTimeEnd
})

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 appendFilterParams(target: RecordQueryParams) {
  // if (queryForm.xxx) target.xxx = queryForm.xxx
  if (queryForm.timeRange?.length === 2) {
    target.createTimeBegin = queryForm.timeRange[0]
    target.createTimeEnd = queryForm.timeRange[1]
  }
}

function buildParams(): RecordQueryParams {
  const p: RecordQueryParams = {
    pageNum: pageNum.value,
    pageSize: pageSize.value,
  }
  appendFilterParams(p)
  return p
}

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

function onReset() {
  // ...清空其它 queryForm 字段
  queryForm.timeRange = null // 同步取消 CreateTimeRangeFilter「今天」
  pageNum.value = 1
  pageSize.value = 10
  fetchList()
}
```

---

## 3.1 创建时间 +「今天」(`CreateTimeRangeFilter`)

- 组件:`@/components/filter/CreateTimeRangeFilter.vue`
- 工具:`getTodayDateTimeRange()`、`isTodayDateTimeRange()`(`@/utils/datetime.ts`)
- 勾选「今天」→ `[YYYY-MM-DD 00:00:00, YYYY-MM-DD 23:59:59]`;取消 → `null`
- 手动选日期与「今天」勾选双向同步;**不要**在列表页再写一套 today 逻辑

---

## 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