729 lines
22 KiB
Markdown
729 lines
22 KiB
Markdown
# Drawer Table Readability 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:** Make all ERP drawers easier to read by eliminating ugly horizontal scrolling, vertical table headers, random wrapping, and overloaded drawer tables.
|
|
|
|
**Architecture:** First add global drawer/table readability guardrails so existing drawers immediately stop collapsing columns. Then introduce a reusable `DrawerDataTable` component for drawer tables with fixed layout, compact labels, ellipsis cells, optional expanded detail rows, and consistent column types. Finally migrate the highest-impact warehouse product overview and inventory ledger drawer tables, leaving the component ready for the rest of the ERP.
|
|
|
|
**Tech Stack:** Vue 3 single-file components, existing `FormDrawer`, existing `TableControls`, existing `PaginationBar`, global CSS in `frontend/src/styles/main.css`, Vite build.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
- Modify: `frontend/src/components/FormDrawer.vue`
|
|
- Add a `size` prop while keeping the old `wide` prop working.
|
|
- Supported sizes: `normal`, `wide`, `xl`, `fullscreen`.
|
|
|
|
- Create: `frontend/src/components/DrawerDataTable.vue`
|
|
- Reusable drawer table renderer.
|
|
- Accepts rows, columns, row key, empty text, and optional expandable row details.
|
|
- Provides named cell slots like `cell-lot_no` and `cell-status`.
|
|
|
|
- Modify: `frontend/src/styles/main.css`
|
|
- Add drawer width classes.
|
|
- Add drawer table readability CSS.
|
|
- Prevent vertical Chinese headers.
|
|
- Add cell helpers for ellipsis, numeric alignment, chip-like long codes, and detail grids.
|
|
|
|
- Modify: `frontend/src/views/InventoryLedgerView.vue`
|
|
- Use `size="fullscreen"` or `size="xl"` for complex inventory detail drawers.
|
|
- Convert the product overview lot table, inventory batch detail table, and inventory transaction table to `DrawerDataTable`.
|
|
- Reduce visible columns to the core fields and move secondary fields to expandable detail.
|
|
|
|
- Test: `frontend/scripts/test-drawer-data-table.mjs`
|
|
- Static test that confirms the new component exists and includes required slot/column/detail behavior.
|
|
|
|
- Test: existing `frontend/scripts/test-inventory-ledger-options.mjs`
|
|
- Must still pass.
|
|
|
|
---
|
|
|
|
### Task 1: Add Drawer Size Levels
|
|
|
|
**Files:**
|
|
- Modify: `frontend/src/components/FormDrawer.vue`
|
|
- Modify: `frontend/src/styles/main.css`
|
|
|
|
- [ ] **Step 1: Update `FormDrawer.vue` props and class binding**
|
|
|
|
Replace the current aside class binding:
|
|
|
|
```vue
|
|
<aside class="workflow-drawer form-drawer" :class="{ 'form-drawer-wide': wide }">
|
|
```
|
|
|
|
with:
|
|
|
|
```vue
|
|
<aside class="workflow-drawer form-drawer" :class="drawerClass">
|
|
```
|
|
|
|
Add this script code after `const emit = defineEmits(["close"]);`:
|
|
|
|
```js
|
|
const drawerClass = computed(() => {
|
|
const normalizedSize = ["normal", "wide", "xl", "fullscreen"].includes(props.size) ? props.size : "normal";
|
|
return {
|
|
"form-drawer-wide": props.wide || normalizedSize === "wide",
|
|
"form-drawer-xl": normalizedSize === "xl",
|
|
"form-drawer-fullscreen": normalizedSize === "fullscreen"
|
|
};
|
|
});
|
|
```
|
|
|
|
Update the import:
|
|
|
|
```js
|
|
import { computed, toRef } from "vue";
|
|
```
|
|
|
|
Add this prop:
|
|
|
|
```js
|
|
size: {
|
|
type: String,
|
|
default: "normal"
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Add drawer width CSS**
|
|
|
|
Append near the existing drawer/form-drawer CSS in `frontend/src/styles/main.css`:
|
|
|
|
```css
|
|
.form-drawer-xl {
|
|
width: min(1280px, calc(100vw - 40px));
|
|
max-width: calc(100vw - 40px);
|
|
}
|
|
|
|
.form-drawer-fullscreen {
|
|
width: min(1680px, calc(100vw - 28px));
|
|
max-width: calc(100vw - 28px);
|
|
height: min(940px, calc(100vh - 28px));
|
|
}
|
|
|
|
.form-drawer-fullscreen .form-drawer-body,
|
|
.form-drawer-xl .form-drawer-body {
|
|
min-width: 0;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Build check**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd /Volumes/ForgeFlow-ERP/frontend
|
|
npm run build
|
|
```
|
|
|
|
Expected: build succeeds.
|
|
|
|
---
|
|
|
|
### Task 2: Add Global Drawer Table Guardrails
|
|
|
|
**Files:**
|
|
- Modify: `frontend/src/styles/main.css`
|
|
|
|
- [ ] **Step 1: Add drawer table readability CSS**
|
|
|
|
Append after the existing `.data-table` styles:
|
|
|
|
```css
|
|
.workflow-drawer .table-wrap {
|
|
max-width: 100%;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.workflow-drawer .data-table {
|
|
min-width: 0;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
.workflow-drawer .data-table th {
|
|
writing-mode: horizontal-tb;
|
|
white-space: normal;
|
|
word-break: keep-all;
|
|
overflow-wrap: normal;
|
|
line-height: 1.35;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.workflow-drawer .data-table td {
|
|
min-width: 0;
|
|
vertical-align: middle;
|
|
word-break: normal;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.drawer-cell-ellipsis {
|
|
display: block;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.drawer-cell-code {
|
|
display: inline-flex;
|
|
max-width: 100%;
|
|
padding: 4px 8px;
|
|
border-radius: 999px;
|
|
background: #eef6ff;
|
|
color: #1f5fa8;
|
|
font-weight: 650;
|
|
line-height: 1.25;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.drawer-cell-number {
|
|
text-align: right;
|
|
font-variant-numeric: tabular-nums;
|
|
white-space: nowrap;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Verify screenshots manually**
|
|
|
|
Run the frontend, open an existing drawer, and confirm:
|
|
|
|
```bash
|
|
cd /Volumes/ForgeFlow-ERP/frontend
|
|
npm run dev
|
|
```
|
|
|
|
Expected:
|
|
- Headers no longer display one Chinese character per line.
|
|
- Tables inside drawers no longer force immediate horizontal scroll for normal-width content.
|
|
- Long batch numbers are allowed to truncate rather than deform the whole table.
|
|
|
|
---
|
|
|
|
### Task 3: Create `DrawerDataTable`
|
|
|
|
**Files:**
|
|
- Create: `frontend/src/components/DrawerDataTable.vue`
|
|
- Create: `frontend/scripts/test-drawer-data-table.mjs`
|
|
|
|
- [ ] **Step 1: Write the static component test**
|
|
|
|
Create `frontend/scripts/test-drawer-data-table.mjs`:
|
|
|
|
```js
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
const componentPath = resolve(process.cwd(), "src/components/DrawerDataTable.vue");
|
|
const source = readFileSync(componentPath, "utf8");
|
|
|
|
const requiredSnippets = [
|
|
"defineProps",
|
|
"columns",
|
|
"rows",
|
|
"rowKey",
|
|
"expandedRows",
|
|
"cellClass",
|
|
"slot :name=\"`cell-${column.key}`\"",
|
|
"drawer-data-table",
|
|
"drawer-row-detail"
|
|
];
|
|
|
|
for (const snippet of requiredSnippets) {
|
|
if (!source.includes(snippet)) {
|
|
throw new Error(`DrawerDataTable missing required snippet: ${snippet}`);
|
|
}
|
|
}
|
|
|
|
console.log("drawer data table static tests passed");
|
|
```
|
|
|
|
- [ ] **Step 2: Run the test to verify it fails**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd /Volumes/ForgeFlow-ERP/frontend
|
|
node scripts/test-drawer-data-table.mjs
|
|
```
|
|
|
|
Expected: FAIL because `src/components/DrawerDataTable.vue` does not exist.
|
|
|
|
- [ ] **Step 3: Create the component**
|
|
|
|
Create `frontend/src/components/DrawerDataTable.vue`:
|
|
|
|
```vue
|
|
<template>
|
|
<div class="drawer-data-table-wrap">
|
|
<table class="data-table compact-table drawer-data-table">
|
|
<colgroup>
|
|
<col v-for="column in visibleColumns" :key="column.key" :style="{ width: column.width || 'auto' }" />
|
|
<col v-if="expandable" style="width: 72px" />
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th v-for="column in visibleColumns" :key="column.key" :title="column.title || column.label">
|
|
<span class="drawer-table-head-label">{{ column.compactLabel || column.label }}</span>
|
|
</th>
|
|
<th v-if="expandable">详情</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template v-for="row in rows" :key="getRowKey(row)">
|
|
<tr>
|
|
<td v-for="column in visibleColumns" :key="column.key" :class="cellClass(column)">
|
|
<slot :name="`cell-${column.key}`" :row="row" :column="column" :value="row[column.key]">
|
|
<span :class="fallbackClass(column)" :title="formatFallback(row, column)">
|
|
{{ formatFallback(row, column) }}
|
|
</span>
|
|
</slot>
|
|
</td>
|
|
<td v-if="expandable" class="drawer-cell-actions">
|
|
<button class="mini-icon-button" type="button" @click="toggleRow(row)">
|
|
{{ isExpanded(row) ? "收起" : "展开" }}
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="expandable && isExpanded(row)" class="drawer-row-detail">
|
|
<td :colspan="visibleColumns.length + 1">
|
|
<slot name="detail" :row="row">
|
|
<div class="drawer-row-detail-grid">
|
|
<article v-for="column in detailColumns" :key="column.key">
|
|
<span>{{ column.label }}</span>
|
|
<strong>{{ formatFallback(row, column) }}</strong>
|
|
</article>
|
|
</div>
|
|
</slot>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
<tr v-if="!rows.length">
|
|
<td :colspan="visibleColumns.length + (expandable ? 1 : 0)" class="empty-row">{{ emptyText }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from "vue";
|
|
|
|
const props = defineProps({
|
|
rows: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
columns: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
rowKey: {
|
|
type: [String, Function],
|
|
default: "id"
|
|
},
|
|
expandedRows: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
emptyText: {
|
|
type: String,
|
|
default: "暂无数据"
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(["update:expandedRows"]);
|
|
|
|
const visibleColumns = computed(() => props.columns.filter((column) => !column.hidden && !column.detailOnly));
|
|
const detailColumns = computed(() => props.columns.filter((column) => column.detailOnly));
|
|
const expandable = computed(() => detailColumns.value.length > 0);
|
|
|
|
function getRowKey(row) {
|
|
return typeof props.rowKey === "function" ? props.rowKey(row) : row?.[props.rowKey];
|
|
}
|
|
|
|
function isExpanded(row) {
|
|
return props.expandedRows.includes(getRowKey(row));
|
|
}
|
|
|
|
function toggleRow(row) {
|
|
const key = getRowKey(row);
|
|
const nextRows = isExpanded(row) ? props.expandedRows.filter((item) => item !== key) : [...props.expandedRows, key];
|
|
emit("update:expandedRows", nextRows);
|
|
}
|
|
|
|
function cellClass(column) {
|
|
return {
|
|
"drawer-cell-number": column.type === "number" || column.align === "right",
|
|
"drawer-cell-status": column.type === "status",
|
|
"drawer-cell-long": column.type === "code" || column.type === "longText"
|
|
};
|
|
}
|
|
|
|
function fallbackClass(column) {
|
|
return {
|
|
"drawer-cell-code": column.type === "code",
|
|
"drawer-cell-ellipsis": column.type === "longText" || column.type === "code"
|
|
};
|
|
}
|
|
|
|
function formatFallback(row, column) {
|
|
if (typeof column.format === "function") {
|
|
return column.format(row[column.key], row);
|
|
}
|
|
const value = row?.[column.key];
|
|
return value === null || value === undefined || value === "" ? "-" : String(value);
|
|
}
|
|
</script>
|
|
```
|
|
|
|
- [ ] **Step 4: Add component CSS**
|
|
|
|
Append to `frontend/src/styles/main.css`:
|
|
|
|
```css
|
|
.drawer-data-table-wrap {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.drawer-data-table {
|
|
width: 100%;
|
|
min-width: 0;
|
|
table-layout: fixed;
|
|
}
|
|
|
|
.drawer-data-table th,
|
|
.drawer-data-table td {
|
|
min-width: 0;
|
|
}
|
|
|
|
.drawer-table-head-label {
|
|
display: inline-block;
|
|
max-width: 100%;
|
|
line-height: 1.35;
|
|
white-space: normal;
|
|
word-break: keep-all;
|
|
}
|
|
|
|
.drawer-cell-actions {
|
|
text-align: center;
|
|
}
|
|
|
|
.mini-icon-button {
|
|
border: 1px solid #d9e6f5;
|
|
border-radius: 999px;
|
|
background: #f5f9ff;
|
|
color: #2f74c8;
|
|
font-size: 12px;
|
|
padding: 4px 9px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.drawer-row-detail td {
|
|
background: #f8fbff;
|
|
}
|
|
|
|
.drawer-row-detail-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 10px;
|
|
}
|
|
|
|
.drawer-row-detail-grid article {
|
|
display: grid;
|
|
gap: 4px;
|
|
padding: 10px 12px;
|
|
border: 1px solid #e4edf7;
|
|
border-radius: 12px;
|
|
background: #ffffff;
|
|
}
|
|
|
|
.drawer-row-detail-grid span {
|
|
color: #7a8699;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.drawer-row-detail-grid strong {
|
|
color: #1f2d3d;
|
|
font-size: 13px;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Run static component test**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd /Volumes/ForgeFlow-ERP/frontend
|
|
node scripts/test-drawer-data-table.mjs
|
|
```
|
|
|
|
Expected: PASS with `drawer data table static tests passed`.
|
|
|
|
---
|
|
|
|
### Task 4: Migrate Warehouse Drawer Tables
|
|
|
|
**Files:**
|
|
- Modify: `frontend/src/views/InventoryLedgerView.vue`
|
|
|
|
- [ ] **Step 1: Import `DrawerDataTable` and add expansion state**
|
|
|
|
Add import:
|
|
|
|
```js
|
|
import DrawerDataTable from "../components/DrawerDataTable.vue";
|
|
```
|
|
|
|
Add refs near the other table page refs:
|
|
|
|
```js
|
|
const expandedLotRows = ref([]);
|
|
const expandedTxnRows = ref([]);
|
|
```
|
|
|
|
- [ ] **Step 2: Add column definitions**
|
|
|
|
Add computed columns near the table-related computed values:
|
|
|
|
```js
|
|
const lotDrawerColumns = computed(() => [
|
|
{ key: "lot_no", label: "库存批次号", compactLabel: "批次号", type: "code", width: "220px" },
|
|
...(isSelectedFinishedWarehouse.value ? [{ key: "inbound_qty", label: "入库数量", compactLabel: "入库数", type: "number", width: "96px", format: (_value, row) => formatQty(row.inbound_qty) }] : []),
|
|
...(selectedWeightOnly.value ? [] : [{ key: "remaining_qty", label: isSelectedFinishedWarehouse.value ? "剩余数量" : "变动数量", compactLabel: isSelectedFinishedWarehouse.value ? "剩余数" : "变动数", type: "number", width: "96px", format: (_value, row) => selectedWarehouseType.value === "SEMI" ? formatSemiMeasureValue(row, row.remaining_qty, "PIECE") : formatQty(row.remaining_qty) }]),
|
|
{ key: "inbound_weight_kg", label: "入库重量(kg)", compactLabel: "入库重量", type: "number", width: "110px", format: (_value, row) => selectedWarehouseType.value === "SEMI" ? formatSemiMeasureValue(row, row.inbound_weight_kg, "WEIGHT") : formatWeight(row.inbound_weight_kg) },
|
|
{ key: "remaining_weight_kg", label: "剩余重量(kg)", compactLabel: "剩余重量", type: "number", width: "110px", format: (_value, row) => selectedWarehouseType.value === "SEMI" ? formatSemiMeasureValue(row, row.remaining_weight_kg, "WEIGHT") : formatWeight(row.remaining_weight_kg) },
|
|
{ key: "unit_cost", label: selectedUnitCostColumnLabel.value, compactLabel: isSelectedFinishedWarehouse.value ? "材料成本" : "单价", type: "number", width: "116px", format: (_value, row) => formatSelectedUnitCost(row.unit_cost) },
|
|
{ key: "status", label: "状态", type: "status", width: "90px" },
|
|
{ key: "warehouse_name", label: "仓库", detailOnly: true },
|
|
{ key: "source_purchase_order_no", label: "采购单号", detailOnly: true },
|
|
{ key: "source_receipt_no", label: "入库单号", detailOnly: true },
|
|
{ key: "source_material_summary", label: "来源库存批次", detailOnly: true },
|
|
{ key: "logistics_waybill_no", label: "运单号", detailOnly: true },
|
|
{ key: "logistics_freight_amount", label: "运费", detailOnly: true, format: (_value, row) => formatFreight(row.logistics_freight_amount) },
|
|
{ key: "quality_release", label: "质检放行人", detailOnly: true, format: (_value, row) => row.item_type === "RAW_MATERIAL" ? formatLotQualityRelease(row) : "-" }
|
|
]);
|
|
|
|
const txnDrawerColumns = computed(() => [
|
|
{ key: "biz_time", label: "时间", type: "longText", width: "128px", format: (_value, row) => formatDateTime(row.biz_time) },
|
|
{ key: "txn_type", label: "类型", width: "94px", format: (_value, row) => formatInventoryTxnTypeLabel(row.txn_type) },
|
|
{ key: "lot_no", label: "库存批次号", compactLabel: "批次号", type: "code", width: "220px" },
|
|
...(selectedWeightOnly.value ? [] : [{ key: "qty_change", label: "变动数量", compactLabel: "变动数", type: "number", width: "96px", format: (_value, row) => formatQty(row.qty_change) }]),
|
|
{ key: "weight_change_kg", label: "变动重量(kg)", compactLabel: "变动重量", type: "number", width: "110px", format: (_value, row) => formatWeight(row.weight_change_kg) },
|
|
{ key: "unit_cost", label: isSelectedFinishedWarehouse.value ? "材料成本" : "单价", type: "number", width: "116px", format: (_value, row) => formatSelectedUnitCost(row.unit_cost) },
|
|
{ key: "amount", label: "金额", type: "number", width: "104px", format: (_value, row) => `¥${formatAmount(row.amount)}` },
|
|
{ key: "source", label: "来源", detailOnly: true, format: (_value, row) => `${formatSourceDocTypeLabel(row.source_doc_type)} #${row.source_doc_id || "-"}` },
|
|
{ key: "remark", label: "备注", detailOnly: true }
|
|
]);
|
|
```
|
|
|
|
- [ ] **Step 3: Replace the lot detail table**
|
|
|
|
Replace the current `库存批次明细` table markup with:
|
|
|
|
```vue
|
|
<DrawerDataTable
|
|
v-model:expanded-rows="expandedLotRows"
|
|
:rows="paginatedLots"
|
|
:columns="lotDrawerColumns"
|
|
row-key="lot_id"
|
|
:empty-text="`暂无该${selectedDetailEntityLabel}库存批次`"
|
|
>
|
|
<template #cell-status="{ row }">
|
|
<span class="status-pill" :class="lotStatusClass(row)">{{ formatStatusLabel(row.status) }}</span>
|
|
</template>
|
|
<template #cell-lot_no="{ row }">
|
|
<span class="drawer-cell-code" :title="row.lot_no">{{ row.lot_no || "-" }}</span>
|
|
</template>
|
|
<template #detail="{ row }">
|
|
<div class="drawer-row-detail-grid">
|
|
<article><span>仓库</span><strong>{{ row.warehouse_name || "-" }}</strong></article>
|
|
<article><span>采购单号</span><strong>{{ row.source_purchase_order_no || "-" }}</strong></article>
|
|
<article><span>入库单号</span><strong>{{ row.source_receipt_no || "-" }}</strong></article>
|
|
<article><span>来源库存批次</span><strong>{{ row.source_material_summary || formatLotSourceLots(row) }}</strong></article>
|
|
<article><span>运单号</span><strong>{{ row.logistics_waybill_no || "-" }}</strong></article>
|
|
<article><span>运费</span><strong>{{ formatFreight(row.logistics_freight_amount) }}</strong></article>
|
|
<article><span>质检放行人</span><strong>{{ row.item_type === "RAW_MATERIAL" ? formatLotQualityRelease(row) : "-" }}</strong></article>
|
|
</div>
|
|
</template>
|
|
</DrawerDataTable>
|
|
```
|
|
|
|
- [ ] **Step 4: Replace the transaction table**
|
|
|
|
Replace the current `库存流水` table markup with:
|
|
|
|
```vue
|
|
<DrawerDataTable
|
|
v-model:expanded-rows="expandedTxnRows"
|
|
:rows="paginatedTransactions"
|
|
:columns="txnDrawerColumns"
|
|
row-key="inventory_txn_id"
|
|
:empty-text="`暂无该${selectedDetailEntityLabel}库存流水`"
|
|
>
|
|
<template #cell-lot_no="{ row }">
|
|
<span class="drawer-cell-code" :title="row.lot_no || ''">{{ row.lot_no || "-" }}</span>
|
|
</template>
|
|
<template #detail="{ row }">
|
|
<div class="drawer-row-detail-grid">
|
|
<article><span>来源</span><strong>{{ formatSourceDocTypeLabel(row.source_doc_type) }} #{{ row.source_doc_id || "-" }}</strong></article>
|
|
<article><span>备注</span><strong>{{ row.remark || "-" }}</strong></article>
|
|
</div>
|
|
</template>
|
|
</DrawerDataTable>
|
|
```
|
|
|
|
- [ ] **Step 5: Use a larger drawer size for complex inventory detail**
|
|
|
|
Find the warehouse detail drawer container in `InventoryLedgerView.vue`. If it uses a custom dual drawer mask, add a class that enables fullscreen width:
|
|
|
|
```vue
|
|
<div v-if="detailDrawerOpen" class="workflow-drawer-mask dual-drawer-mask inventory-ledger-detail-mask" @click.self="closeDetailDrawer">
|
|
```
|
|
|
|
Add CSS:
|
|
|
|
```css
|
|
.inventory-ledger-detail-mask .dual-drawer-layout {
|
|
width: min(1760px, calc(100vw - 28px));
|
|
max-width: calc(100vw - 28px);
|
|
}
|
|
|
|
.inventory-ledger-detail-mask .workflow-drawer {
|
|
min-width: 0;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 6: Run focused frontend tests**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd /Volumes/ForgeFlow-ERP/frontend
|
|
node scripts/test-drawer-data-table.mjs
|
|
node scripts/test-inventory-ledger-options.mjs
|
|
npm run build
|
|
```
|
|
|
|
Expected:
|
|
- Static drawer table test passes.
|
|
- Existing inventory option test passes.
|
|
- Vite build passes.
|
|
|
|
---
|
|
|
|
### Task 5: Apply the Same Pattern to Other Drawers
|
|
|
|
**Files:**
|
|
- Modify high-impact drawer pages after warehouse migration:
|
|
- `frontend/src/views/PurchaseOrderView.vue`
|
|
- `frontend/src/views/DeliveryManagementView.vue`
|
|
- `frontend/src/views/WorkOrderManagementView.vue`
|
|
- `frontend/src/views/CompletionReceiptView.vue`
|
|
- `frontend/src/components/ProductWorkflowDrawer.vue`
|
|
|
|
- [ ] **Step 1: Identify drawer tables that exceed 7 columns**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd /Volumes/ForgeFlow-ERP
|
|
rg -n "<table class=\"data-table|<table class=\"data-table compact-table" frontend/src/views frontend/src/components
|
|
```
|
|
|
|
Expected: list all drawer tables. Prioritize tables inside `FormDrawer`, `.workflow-drawer`, or custom drawer masks.
|
|
|
|
- [ ] **Step 2: For each drawer table, classify columns**
|
|
|
|
Use this rule:
|
|
|
|
```text
|
|
Main table: 5-7 business-critical columns.
|
|
Detail row: long text, source documents, remarks, logistics, photos, secondary IDs.
|
|
Status: always visible as colored pill.
|
|
Money/quantity/weight: visible only when it is part of the user's immediate decision.
|
|
```
|
|
|
|
- [ ] **Step 3: Migrate one page at a time**
|
|
|
|
For each target page:
|
|
|
|
1. Import `DrawerDataTable`.
|
|
2. Add `expandedRows` ref.
|
|
3. Add `columns` computed.
|
|
4. Replace raw drawer table with `DrawerDataTable`.
|
|
5. Keep existing `TableControls` and `PaginationBar`.
|
|
6. Run `npm run build`.
|
|
|
|
Example import and state:
|
|
|
|
```js
|
|
import DrawerDataTable from "../components/DrawerDataTable.vue";
|
|
|
|
const expandedRows = ref([]);
|
|
```
|
|
|
|
- [ ] **Step 4: Stop when all common drawers follow the new pattern**
|
|
|
|
Acceptance criteria:
|
|
- No drawer table header displays vertically.
|
|
- No drawer table requires horizontal scrolling for the default visible columns.
|
|
- Long codes are shown as chips or ellipsis with `title`.
|
|
- Secondary details are visible by expanding the row.
|
|
|
|
---
|
|
|
|
### Task 6: Verification
|
|
|
|
**Files:**
|
|
- Test: `frontend/scripts/test-drawer-data-table.mjs`
|
|
- Test: `frontend/scripts/test-inventory-ledger-options.mjs`
|
|
|
|
- [ ] **Step 1: Run static and build checks**
|
|
|
|
Run:
|
|
|
|
```bash
|
|
cd /Volumes/ForgeFlow-ERP/frontend
|
|
node scripts/test-drawer-data-table.mjs
|
|
node scripts/test-inventory-ledger-options.mjs
|
|
npm run build
|
|
```
|
|
|
|
Expected: all pass.
|
|
|
|
- [ ] **Step 2: Manual browser verification**
|
|
|
|
Open the ERP in the browser and check:
|
|
|
|
```text
|
|
1. 百华仓库 -> 成品库 -> 产品明细抽屉
|
|
2. 库存批次明细表
|
|
3. 库存流水表
|
|
4. 采购订单详情抽屉
|
|
5. 发货/销售出库详情抽屉
|
|
```
|
|
|
|
Expected:
|
|
- No vertical table headers.
|
|
- No ugly horizontal scrollbar for normal data.
|
|
- Long batch numbers do not deform the table.
|
|
- Row expand details reveal hidden fields clearly.
|
|
- Pagination bar remains visually consistent.
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
- Spec coverage: The plan covers drawer width, table header readability, horizontal-scroll reduction, long-code handling, detail expansion, and migration of warehouse plus other high-impact drawers.
|
|
- Placeholder scan: No `TBD`, `TODO`, or unspecified “handle later” steps remain.
|
|
- Type consistency: `DrawerDataTable` props are consistently named `rows`, `columns`, `rowKey`, `expandedRows`, and `emptyText`; slot names use `cell-${column.key}` and `detail`.
|
|
- Risk note: The current project directory is mounted at `/Volumes/ForgeFlow-ERP` and is not detected as a git repository, so commit steps are intentionally omitted.
|