Align Fresh Mint §7.6 and OpenSpec ui-conventions with the confirmed UX; include design and plan. Co-authored-by: Cursor <cursoragent@cursor.com>
651 lines
20 KiB
Markdown
651 lines
20 KiB
Markdown
# web-admin 列表表体内嵌滚动(视口填满)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:** 让 web-admin 主列表在视口内表体内嵌滚动:表头固定、横滚条贴在表体可视区底,分页始终可见且距内容区底 10px。
|
||
|
||
**Architecture:** 抽出纯函数 `calcListTableViewportHeight`;composable `useListTableViewportHeight` 测量 `#app-content` 底、表顶、分页高并绑定 `ListTable`/`el-table` 的 `:height`;先接模板页与 bx 样板,再文档/OpenSpec,最后按模块批推。
|
||
|
||
**Tech Stack:** Vue 3 · `@vueuse/core` · Element Plus `el-table` height · TypeScript · `tsx` 脚本单测
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-09-01-web-admin-list-table-viewport-scroll-design.md`
|
||
|
||
**Demo:** `docs/list-table-scroll-demo.html`
|
||
|
||
---
|
||
|
||
## File map
|
||
|
||
| File | Responsibility |
|
||
|------|----------------|
|
||
| `web-admin/src/composables/listTableViewportHeight.ts` | 纯函数:高度公式 + 默认常量 |
|
||
| `web-admin/scripts/check-list-table-viewport-height.ts` | 无浏览器依赖的公式断言(tsx) |
|
||
| `web-admin/src/composables/useListTableViewportHeight.ts` | 测量 DOM、resize/观察、返回 `tableHeight` + `recalc` |
|
||
| `web-admin/src/views/demo/unit/index.vue` | 模板样板页接入 |
|
||
| `web-admin/src/views/bx/reimburse/apply/index.vue` | ListTable + wrap 样板接入 |
|
||
| `web-admin/src/styles/bx/index.scss` | 澄清 `.table-scroll` 与 EP height 模式不双滚 |
|
||
| `docs/web-admin-ui-spec-fresh-mint.md` §7.6 | 规范改为视口填满策略 |
|
||
| `docs/list-table-scroll-demo.html` | Demo 文案对齐「分页下 10px」 |
|
||
| `openspec/.../ui-conventions/spec.md` | 新增 SHALL |
|
||
| 后续批处理列表页 | 同构接入(Task 6 清单) |
|
||
|
||
---
|
||
|
||
### Task 1: 纯函数高度公式 + 脚本单测
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/composables/listTableViewportHeight.ts`
|
||
- Create: `web-admin/scripts/check-list-table-viewport-height.ts`
|
||
|
||
- [ ] **Step 1: 写失败断言脚本(文件尚不存在时应无法通过)**
|
||
|
||
创建 `web-admin/scripts/check-list-table-viewport-height.ts`:
|
||
|
||
```ts
|
||
import assert from 'node:assert/strict'
|
||
import {
|
||
LIST_TABLE_VIEWPORT_BOTTOM_GAP,
|
||
LIST_TABLE_VIEWPORT_MIN_HEIGHT,
|
||
calcListTableViewportHeight
|
||
} from '../src/composables/listTableViewportHeight'
|
||
|
||
assert.equal(LIST_TABLE_VIEWPORT_BOTTOM_GAP, 10)
|
||
assert.equal(LIST_TABLE_VIEWPORT_MIN_HEIGHT, 240)
|
||
|
||
// contentBottom=1000, tableTop=200, pager=48 → 1000-200-48-10 = 742
|
||
assert.equal(
|
||
calcListTableViewportHeight({
|
||
contentViewportBottom: 1000,
|
||
tableTop: 200,
|
||
pagerHeight: 48
|
||
}),
|
||
742
|
||
)
|
||
|
||
// 无分页
|
||
assert.equal(
|
||
calcListTableViewportHeight({
|
||
contentViewportBottom: 800,
|
||
tableTop: 100,
|
||
pagerHeight: 0
|
||
}),
|
||
690
|
||
)
|
||
|
||
// 过矮时夹到 minHeight
|
||
assert.equal(
|
||
calcListTableViewportHeight({
|
||
contentViewportBottom: 400,
|
||
tableTop: 300,
|
||
pagerHeight: 48,
|
||
minHeight: 240,
|
||
bottomGap: 10
|
||
}),
|
||
240
|
||
)
|
||
|
||
// 自定义 min / gap
|
||
assert.equal(
|
||
calcListTableViewportHeight({
|
||
contentViewportBottom: 1000,
|
||
tableTop: 100,
|
||
pagerHeight: 40,
|
||
minHeight: 100,
|
||
bottomGap: 16
|
||
}),
|
||
844
|
||
)
|
||
|
||
console.log('check-list-table-viewport-height: ok')
|
||
```
|
||
|
||
- [ ] **Step 2: 跑脚本确认失败**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd web-admin && pnpm exec tsx scripts/check-list-table-viewport-height.ts
|
||
```
|
||
|
||
Expected: 失败(模块不存在 / 无法解析)
|
||
|
||
- [ ] **Step 3: 实现纯函数**
|
||
|
||
创建 `web-admin/src/composables/listTableViewportHeight.ts`:
|
||
|
||
```ts
|
||
/** 分页底边到内容区底的默认空隙(px) */
|
||
export const LIST_TABLE_VIEWPORT_BOTTOM_GAP = 10
|
||
|
||
/** 表体动态高度下限(px),避免筛选展开时表被压没 */
|
||
export const LIST_TABLE_VIEWPORT_MIN_HEIGHT = 240
|
||
|
||
export type CalcListTableViewportHeightInput = {
|
||
/** 主内容滚动容器可视底边(如 #app-content.getBoundingClientRect().bottom) */
|
||
contentViewportBottom: number
|
||
/** 表格锚点顶边(同坐标系,通常 getBoundingClientRect().top) */
|
||
tableTop: number
|
||
/** 分页条实际高度;无分页传 0 */
|
||
pagerHeight: number
|
||
minHeight?: number
|
||
bottomGap?: number
|
||
}
|
||
|
||
/**
|
||
* 计算列表表体应设置的 height,使「表 + 分页」落在内容视口内,且分页下方留 bottomGap。
|
||
* tableHeight = floor(contentBottom - tableTop - pagerHeight - bottomGap),再与 minHeight 取大。
|
||
*/
|
||
export function calcListTableViewportHeight(
|
||
input: CalcListTableViewportHeightInput
|
||
): number {
|
||
const minHeight = input.minHeight ?? LIST_TABLE_VIEWPORT_MIN_HEIGHT
|
||
const bottomGap = input.bottomGap ?? LIST_TABLE_VIEWPORT_BOTTOM_GAP
|
||
const raw = Math.floor(
|
||
input.contentViewportBottom - input.tableTop - input.pagerHeight - bottomGap
|
||
)
|
||
if (!Number.isFinite(raw)) return minHeight
|
||
return Math.max(raw, minHeight)
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 再跑脚本确认通过**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd web-admin && pnpm exec tsx scripts/check-list-table-viewport-height.ts
|
||
```
|
||
|
||
Expected: 打印 `check-list-table-viewport-height: ok`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/composables/listTableViewportHeight.ts \
|
||
web-admin/scripts/check-list-table-viewport-height.ts
|
||
git commit -m "$(cat <<'EOF'
|
||
feat(web-admin): add list table viewport height formula
|
||
|
||
Pure calc keeps pagination visible with a 10px bottom gap for main list scroll UX.
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: composable `useListTableViewportHeight`
|
||
|
||
**Files:**
|
||
- Create: `web-admin/src/composables/useListTableViewportHeight.ts`
|
||
|
||
- [ ] **Step 1: 实现 composable**
|
||
|
||
创建 `web-admin/src/composables/useListTableViewportHeight.ts`:
|
||
|
||
```ts
|
||
import {
|
||
nextTick,
|
||
onBeforeUnmount,
|
||
onMounted,
|
||
ref,
|
||
unref,
|
||
watch,
|
||
type Ref
|
||
} from 'vue'
|
||
import { useEventListener, useResizeObserver } from '@vueuse/core'
|
||
import {
|
||
LIST_TABLE_VIEWPORT_BOTTOM_GAP,
|
||
LIST_TABLE_VIEWPORT_MIN_HEIGHT,
|
||
calcListTableViewportHeight
|
||
} from '@/composables/listTableViewportHeight'
|
||
|
||
export type UseListTableViewportHeightOptions = {
|
||
/** 表格锚点(包住 ListTable/el-table 的元素,不含分页) */
|
||
tableEl: Ref<HTMLElement | null | undefined> | (() => HTMLElement | null | undefined)
|
||
/** 分页条根节点;无分页可省略 */
|
||
pagerEl?: Ref<HTMLElement | null | undefined> | (() => HTMLElement | null | undefined)
|
||
/** 可选:ListTable / el-table 组件实例,高度变化后调用 doLayout */
|
||
tableRef?: Ref<any>
|
||
minHeight?: number
|
||
bottomGap?: number
|
||
/** 主内容滚动根;默认 #app-content */
|
||
scrollRootSelector?: string
|
||
/** 为 false 时不自动监听(隐藏 Tab 可用) */
|
||
enabled?: Ref<boolean> | boolean
|
||
}
|
||
|
||
function resolveEl(
|
||
source?: Ref<HTMLElement | null | undefined> | (() => HTMLElement | null | undefined)
|
||
): HTMLElement | null {
|
||
if (!source) return null
|
||
if (typeof source === 'function') return source() ?? null
|
||
return unref(source) ?? null
|
||
}
|
||
|
||
export function useListTableViewportHeight(options: UseListTableViewportHeightOptions) {
|
||
const tableHeight = ref(LIST_TABLE_VIEWPORT_MIN_HEIGHT)
|
||
const minHeight = options.minHeight ?? LIST_TABLE_VIEWPORT_MIN_HEIGHT
|
||
const bottomGap = options.bottomGap ?? LIST_TABLE_VIEWPORT_BOTTOM_GAP
|
||
const scrollRootSelector = options.scrollRootSelector ?? '#app-content'
|
||
|
||
const isEnabled = () => {
|
||
const e = options.enabled
|
||
if (e === undefined) return true
|
||
return !!unref(e)
|
||
}
|
||
|
||
const recalc = () => {
|
||
if (!isEnabled()) return
|
||
if (typeof window === 'undefined') return
|
||
|
||
const table = resolveEl(options.tableEl)
|
||
if (!table) return
|
||
|
||
const root =
|
||
(document.querySelector(scrollRootSelector) as HTMLElement | null) ?? null
|
||
const contentViewportBottom = root
|
||
? root.getBoundingClientRect().bottom
|
||
: window.innerHeight
|
||
|
||
const tableTop = table.getBoundingClientRect().top
|
||
const pager = resolveEl(options.pagerEl)
|
||
const pagerHeight = pager ? Math.ceil(pager.getBoundingClientRect().height) : 0
|
||
|
||
const next = calcListTableViewportHeight({
|
||
contentViewportBottom,
|
||
tableTop,
|
||
pagerHeight,
|
||
minHeight,
|
||
bottomGap
|
||
})
|
||
|
||
if (next !== tableHeight.value) {
|
||
tableHeight.value = next
|
||
}
|
||
|
||
nextTick(() => {
|
||
const inst = options.tableRef ? unref(options.tableRef) : null
|
||
if (inst && typeof inst.doLayout === 'function') {
|
||
inst.doLayout()
|
||
}
|
||
})
|
||
}
|
||
|
||
let raf = 0
|
||
const scheduleRecalc = () => {
|
||
if (raf) cancelAnimationFrame(raf)
|
||
raf = requestAnimationFrame(() => {
|
||
raf = 0
|
||
recalc()
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
nextTick(scheduleRecalc)
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
if (raf) cancelAnimationFrame(raf)
|
||
})
|
||
|
||
useEventListener(window, 'resize', scheduleRecalc)
|
||
|
||
// 观察表锚点与分页尺寸变化(筛选展开会改变 tableTop)
|
||
useResizeObserver(
|
||
() => resolveEl(options.tableEl),
|
||
() => scheduleRecalc()
|
||
)
|
||
useResizeObserver(
|
||
() => resolveEl(options.pagerEl),
|
||
() => scheduleRecalc()
|
||
)
|
||
useResizeObserver(
|
||
() => document.querySelector(scrollRootSelector) as HTMLElement | null,
|
||
() => scheduleRecalc()
|
||
)
|
||
|
||
if (options.enabled !== undefined && typeof options.enabled !== 'boolean') {
|
||
watch(options.enabled, () => scheduleRecalc())
|
||
}
|
||
|
||
return { tableHeight, recalc: scheduleRecalc }
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: 类型检查(抽查无报错)**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd web-admin && pnpm exec vue-tsc --noEmit 2>&1 | head -n 40
|
||
```
|
||
|
||
Expected: 与本次新文件相关无新增错误(若仓库已有历史错误,确认不含 `useListTableViewportHeight` / `listTableViewportHeight`)
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/composables/useListTableViewportHeight.ts
|
||
git commit -m "$(cat <<'EOF'
|
||
feat(web-admin): add useListTableViewportHeight composable
|
||
|
||
Measure content viewport, table top, and pager to drive el-table height.
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: 接入模板样板页 `demo/unit`
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/demo/unit/index.vue`
|
||
|
||
- [ ] **Step 1: 模板增加锚点 ref,绑定 height**
|
||
|
||
在 `list-body` 内,用 wrap 只包住表格(分页保持为兄弟节点)。将:
|
||
|
||
```vue
|
||
<div class="list-body list-wrap">
|
||
<ListTable table-key="demo-unit"
|
||
class="list-table"
|
||
:data="tableData"
|
||
```
|
||
|
||
改为:
|
||
|
||
```vue
|
||
<div class="list-body list-wrap">
|
||
<div ref="unitTableWrapRef" class="list-table-viewport-anchor">
|
||
<ListTable
|
||
ref="unitTableRef"
|
||
table-key="demo-unit"
|
||
class="list-table"
|
||
:height="tableHeight"
|
||
:data="tableData"
|
||
```
|
||
|
||
在 `ListTable` 闭合标签后、分页前闭合 wrap:
|
||
|
||
```vue
|
||
</ListTable>
|
||
</div>
|
||
<div ref="unitPagerRef" class="pagination list-pager-bar">
|
||
```
|
||
|
||
(原分页 `class="pagination list-pager-bar"` 增加 `ref="unitPagerRef"`。)
|
||
|
||
- [ ] **Step 2: script 接入 composable**
|
||
|
||
在 `<script setup>` 中增加(保持现有 import 风格,路径用 `@/composables/...`):
|
||
|
||
```ts
|
||
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
|
||
|
||
const unitTableWrapRef = ref<HTMLElement | null>(null)
|
||
const unitPagerRef = ref<HTMLElement | null>(null)
|
||
const unitTableRef = ref<any>(null)
|
||
|
||
const { tableHeight, recalc: recalcTableHeight } = useListTableViewportHeight({
|
||
tableEl: unitTableWrapRef,
|
||
pagerEl: unitPagerRef,
|
||
tableRef: unitTableRef
|
||
})
|
||
```
|
||
|
||
若该页有筛选重置/搜索导致布局变化,在 `handleSearch` / `handleReset` / `loadData` 完成后 `nextTick(() => recalcTableHeight())`(可选但推荐至少在 `loadData` 结束 `finally` 里调一次)。
|
||
|
||
- [ ] **Step 3: 手动验收(dev)**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd web-admin && pnpm dev
|
||
```
|
||
|
||
打开 `/#/demo/unit`(或侧栏 Demo → 单位):
|
||
|
||
1. 分页条下方到内容区底约 10px,分页始终可见
|
||
2. 若有足够行数:表体内纵滚,表头不消失
|
||
3. 缩窄窗口使列溢出:横滚条在表体底,无需滚到最后一行
|
||
4. 空数据时表区仍保持较大高度(不塌成一条)
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/views/demo/unit/index.vue
|
||
git commit -m "$(cat <<'EOF'
|
||
feat(web-admin): use viewport table height on demo unit list
|
||
|
||
Pilot the fill-viewport list scroll pattern on the standard list template page.
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: 接入 bx 样板 + 澄清 `.table-scroll`
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/bx/reimburse/apply/index.vue`
|
||
- Modify: `web-admin/src/styles/bx/index.scss`
|
||
|
||
- [ ] **Step 1: 报销申请列表绑定 height**
|
||
|
||
该页已有:
|
||
|
||
```vue
|
||
<div ref="reimburseApplyTableWrapRef" class="table-scroll bx-list-table-wrap">
|
||
<ListTable ...>
|
||
```
|
||
|
||
与分页:
|
||
|
||
```vue
|
||
<div class="pagination list-pager-bar">
|
||
```
|
||
|
||
改动要点:
|
||
|
||
1. `ListTable` 增加 `ref="reimburseApplyTableRef"`、`:height="tableHeight"`
|
||
2. 分页根增加 `ref="reimburseApplyPagerRef"`
|
||
3. script 中:
|
||
|
||
```ts
|
||
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
|
||
|
||
// 已有 reimburseApplyTableWrapRef 则复用
|
||
const reimburseApplyPagerRef = ref<HTMLElement | null>(null)
|
||
const reimburseApplyTableRef = ref<any>(null)
|
||
|
||
const { tableHeight, recalc: recalcTableHeight } = useListTableViewportHeight({
|
||
tableEl: reimburseApplyTableWrapRef,
|
||
pagerEl: reimburseApplyPagerRef,
|
||
tableRef: reimburseApplyTableRef
|
||
})
|
||
```
|
||
|
||
4. 列头筛选 / 工具栏导致布局变化处调用 `recalcTableHeight()`(至少在筛选 `setFilterState` 后 `nextTick` 一次,可用现有 watch 挂载点)。
|
||
|
||
- [ ] **Step 2: 调整 bx `.table-scroll` 注释与行为**
|
||
|
||
将 `web-admin/src/styles/bx/index.scss` 中:
|
||
|
||
```scss
|
||
.table-scroll {
|
||
overflow-x: auto;
|
||
overflow-y: hidden;
|
||
}
|
||
```
|
||
|
||
改为(避免与 EP `height` 内滚双滚动;横向交由 el-table):
|
||
|
||
```scss
|
||
/* 主列表使用 el-table :height 内滚后,wrap 只负责最小宽度收缩,勿再 overflow-y */
|
||
.table-scroll {
|
||
min-width: 0;
|
||
overflow: visible;
|
||
}
|
||
```
|
||
|
||
若个别页仍依赖 wrap 横滚且未设 height,批处理前可暂保留 `overflow-x: auto`,但**已设 height 的页**必须以 EP 为准。推荐全局改为 `overflow: visible` + `min-width: 0`,与 Task 4 样板一起验证。
|
||
|
||
- [ ] **Step 3: 手动验收**
|
||
|
||
打开报销申请列表:固定列、列筛选、分页底隙、表头与横滚条行为同 Task 3。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/views/bx/reimburse/apply/index.vue web-admin/src/styles/bx/index.scss
|
||
git commit -m "$(cat <<'EOF'
|
||
feat(web-admin): viewport-fill scroll on reimburse apply list
|
||
|
||
Wire ListTable height to the shared composable and stop double-scroll on wraps.
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: 规范、Demo、OpenSpec
|
||
|
||
**Files:**
|
||
- Modify: `docs/web-admin-ui-spec-fresh-mint.md`(§7.6)
|
||
- Modify: `docs/list-table-scroll-demo.html`(方案 A 说明对齐)
|
||
- Modify: `openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md`
|
||
|
||
- [ ] **Step 1: 更新 Fresh Mint §7.6**
|
||
|
||
将 §7.6 替换为(保持文档语气与周边章节一致):
|
||
|
||
```markdown
|
||
### 7.6 主列表视口填满滚动
|
||
|
||
主内容区业务列表(`list-body` + 表 + `list-pager-bar`)SHALL 使用 Element Plus 表体固定可视高度(`height` / 动态像素),使:
|
||
|
||
- **纵向滚动发生在表体内**,表头固定在表体顶部;
|
||
- **横向滚动条贴在表体可视区底部**(不必滚到最后一行);
|
||
- **分页条在表外且始终可见**,分页底边到内容区底保留 **10px**。
|
||
|
||
推荐通过 composable `useListTableViewportHeight` 计算:
|
||
|
||
`height = max(240, floor(contentViewportBottom - tableTop - pagerHeight - 10))`
|
||
|
||
测量根优先 `#app-content`。弹窗内明细小表、统计卡内小表不强制本策略,可继续使用局部 `max-height`。
|
||
|
||
历史 HTML demo 类名 **`.table-scroll`** 仅作结构参考;落地以 `el-table`/`ListTable` 的 `height` 为准,避免外层再套冲突的 `overflow-y: auto`。
|
||
```
|
||
|
||
- [ ] **Step 2: OpenSpec 增加 Requirement**
|
||
|
||
在 `openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md` 合适位置(列表相关章节附近)追加:
|
||
|
||
```markdown
|
||
### Requirement: 主列表表体内嵌滚动与视口填满
|
||
|
||
`web-admin` 主内容区带分页的业务列表 SHALL 为 `el-table` / `ListTable` 设置动态可视 `height`,使纵滚与横滚发生在表体内;表头 SHALL 在表体顶部保持可见;分页条 SHALL 保持在表外可见,且分页底边到主内容区底 SHALL 保留约 10px。SHALL NOT 依赖「整表随页面拉长」导致横滚条沉在最后一行之后。弹窗内嵌套小表 MAY 使用局部 `max-height`,不强制本条。
|
||
|
||
#### Scenario: 新增长列表页
|
||
|
||
- **WHEN** 在 `web-admin` 新增主内容区业务列表页(含 `list-pager-bar`)
|
||
- **THEN** 表格 SHALL 使用视口剩余高度策略(或等价 `useListTableViewportHeight`),且分页下方保留约 10px 底隙
|
||
|
||
#### Scenario: 验收横滚与表头
|
||
|
||
- **WHEN** 列表行数与列宽足以产生纵滚与横滚
|
||
- **THEN** 用户纵滚到表体中部时表头仍可见,且无需滚到最后一行即可操作横向滚动条
|
||
```
|
||
|
||
- [ ] **Step 3: Demo 文案**
|
||
|
||
更新 `docs/list-table-scroll-demo.html` 中方案 A 的描述,明确「分页下方 10px、分页始终可见」(若已写则可微调数字/文案一致)。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add docs/web-admin-ui-spec-fresh-mint.md \
|
||
docs/list-table-scroll-demo.html \
|
||
openspec/changes/zkxw-engineering-standards/specs/ui-conventions/spec.md
|
||
git commit -m "$(cat <<'EOF'
|
||
docs: specify viewport-fill list table scrolling
|
||
|
||
Align Fresh Mint §7.6 and OpenSpec ui-conventions with the confirmed UX.
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: 批处理推广(同构列表)
|
||
|
||
**Files(按批修改,模式同 Task 3/4):**
|
||
|
||
优先批(已有 `table-scroll` / `*TableWrapRef` + `ListTable`):
|
||
|
||
- `web-admin/src/views/bx/view/{mine,todo,all}/index.vue`
|
||
- `web-admin/src/views/bx/reimburse/approve/index.vue`(双表:每个 Tab 各自 height + `enabled`)
|
||
- `web-admin/src/views/bx/advance/**/index.vue`
|
||
- `web-admin/src/views/bx/payment/index.vue`
|
||
- `web-admin/src/views/pl/plan/index.vue`、`plan-archive`、`freight/components/*Panel.vue`
|
||
- `web-admin/src/views/db/{weighing,inventory,plans}/index.vue`
|
||
- `web-admin/src/views/demo/{order,product,user,category}/index.vue`(模板族)
|
||
|
||
每页最小改动清单:
|
||
|
||
1. 表外包/已有 wrap:`ref` 测顶
|
||
2. 分页:`ref` 测高
|
||
3. `ListTable`/`el-table`:`:height="tableHeight"` + 可选 `ref` 供 `doLayout`
|
||
4. `useListTableViewportHeight({ tableEl, pagerEl, tableRef })`
|
||
5. Tab 多表:`enabled: computed(() => activeTab === 'xxx')`
|
||
6. 勿给 wrap 再加与 height 冲突的 `overflow-y: auto`
|
||
|
||
- [ ] **Step 1: 按模块批量改并本地点验 2~3 个代表页**
|
||
|
||
- [ ] **Step 2: 每完成一个模块 Commit 一次**(示例)
|
||
|
||
```bash
|
||
git add web-admin/src/views/bx web-admin/src/views/pl web-admin/src/views/db
|
||
git commit -m "$(cat <<'EOF'
|
||
feat(web-admin): roll out viewport list height to bx/pl/db lists
|
||
|
||
Keep headers sticky and horizontal scrollbars in the table viewport.
|
||
EOF
|
||
)"
|
||
```
|
||
|
||
(可拆成多次 commit,按模块更清晰。)
|
||
|
||
- [ ] **Step 3: 跑公式脚本回归**
|
||
|
||
```bash
|
||
cd web-admin && pnpm exec tsx scripts/check-list-table-viewport-height.ts
|
||
```
|
||
|
||
Expected: `ok`
|
||
|
||
---
|
||
|
||
## Spec coverage checklist
|
||
|
||
| Spec 项 | Task |
|
||
|---------|------|
|
||
| 方案 A 表体内嵌滚动 | 2–4, 6 |
|
||
| 分页下 10px / 分页始终可见 | 1 公式, 2 测量, 3–4 验收 |
|
||
| minHeight 240 | 1 |
|
||
| resize / 筛选变化重算 | 2 `useResizeObserver` + 页内 `recalc` |
|
||
| 不进 ListTable 隐式写死 | 2(页显式接入) |
|
||
| 空数据仍撑高度 | 3 用 `height` 非仅 max-height |
|
||
| 规范 §7.6 / OpenSpec | 5 |
|
||
| 非目标 B/C、弹窗小表、小程序 | 未立 task(刻意不做) |
|
||
| 批推广 | 6 |
|
||
|
||
---
|
||
|
||
## Self-review notes
|
||
|
||
- 无 TBD/placeholder。
|
||
- 公式常量与 composable 默认值一致(10 / 240)。
|
||
- 仓库无 vitest:用 `tsx` 脚本测纯函数,UI 用手动验收。
|
||
- `vue-tsc` 全量可能有历史噪声:以「新文件无报错」为准。
|