From ef71e842ee7f4fdf9b68827811c853f19b50d394 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Sun, 18 Oct 2026 16:40:35 +0800 Subject: [PATCH] fix(web-admin): stick category header tools with MpBusinessHeader. Move expand-all and add into a header actions slot so they remain visible while scrolling. Co-authored-by: Cursor --- web-admin/scripts/mp-crm-primitives.test.mjs | 97 +++++++++++++++++++ .../src/components/mp/MpBusinessHeader.vue | 61 ++++++++++++ web-admin/src/styles/mp-webview.scss | 9 ++ .../src/views/m/crm/product/categories.vue | 43 ++++---- 4 files changed, 184 insertions(+), 26 deletions(-) create mode 100644 web-admin/scripts/mp-crm-primitives.test.mjs create mode 100644 web-admin/src/components/mp/MpBusinessHeader.vue diff --git a/web-admin/scripts/mp-crm-primitives.test.mjs b/web-admin/scripts/mp-crm-primitives.test.mjs new file mode 100644 index 000000000..a7cfa0bc2 --- /dev/null +++ b/web-admin/scripts/mp-crm-primitives.test.mjs @@ -0,0 +1,97 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { readFile } from 'node:fs/promises' + +const root = new URL('../', import.meta.url) +const read = (path) => readFile(new URL(path, root), 'utf8') + +test('普通业务头仅提供固定标题与可选搜索,不暴露票根或动态摘要插槽', async () => { + const source = await read('src/components/mp/MpBusinessHeader.vue') + assert.match(source, /class="mp-wv-header"/) + assert.match(source, /class="mp-wv-header__title"[^>]*>\{\{ title \}\}/) + assert.match(source, /v-if="searchable"[\s\S]*?class="mp-wv-search\b/) + assert.match(source, /name="before-search"/) + assert.match(source, /name="after-search"/) + assert.match(source, /\$slots\.actions[\s\S]*?name="actions"/) + assert.doesNotMatch(source, /ticket|hero|mp-wv-header__sub/i) +}) + +test('状态 Tab 与三栏筛选保持独立局部 modifier,并给更多筛选红点', async () => { + const [tabs, filters, legacyStyles, styles] = await Promise.all([ + read('src/components/mp/MpStatusTabs.vue'), + read('src/components/mp/MpThreeColumnFilters.vue'), + read('src/styles/mp-webview.scss'), + read('src/styles/mp-crm-primitives.scss') + ]) + assert.match(tabs, /class="mp-wv-status-tabs"/) + assert.match(tabs, /class="filter-tag"/) + assert.match(tabs, /]*class="mp-wv-status-tabs"/) + assert.match(tabs, /:aria-pressed="tab\.value === modelValue"/) + assert.match(filters, /mp-wv-filter-bar--three/) + assert.match(filters, /mp-wv-filter-more/) + assert.match(filters, /v-if="moreActive"[^>]*class="mp-wv-filter-more__dot"/) + assert.match(legacyStyles, /\.mp-wv-filter-bar\s*\{[\s\S]*?grid-template-columns:\s*1fr 1fr;/) + assert.match( + styles, + /\.mp-wv-filter-bar\.mp-wv-filter-bar--three\s*\{[\s\S]*?grid-template-columns:\s*repeat\(3, minmax\(0, 1fr\)\);/ + ) + assert.match( + styles, + /\.mp-wv-filter-wrap--business \.mp-wv-filter-opt\s*\{[\s\S]*?min-height:\s*44px;/ + ) + assert.match( + styles, + /\.mp-wv-filter-wrap--business > \.mp-wv-filter-mask\s*\{[\s\S]*?z-index:\s*7;/, + '局部遮罩必须低于下拉面板,否则选项无法点击' + ) +}) + +test('备用金式卡片完整声明六层 DOM,状态标签使用浅底语义色', async () => { + const [card, styles] = await Promise.all([ + read('src/components/mp/MpExpenseCard.vue'), + read('src/styles/mp-crm-primitives.scss') + ]) + for (const className of [ + 'expense-card', + 'ec-top', + 'status-tag', + 'ec-amount', + 'ec-emphasis', + 'list-card-kv', + 'ec-time', + 'ec-bottom', + 'op-actions' + ]) { + assert.match(card, new RegExp(`\\b${className}\\b`), `卡片缺少 ${className} 层`) + } + assert.doesNotMatch(card, /= 5, '状态标签应声明基础样式与语义色') + for (const [, declarations] of statusBlocks) { + assert.doesNotMatch(declarations, /color:\s*#fff;/, '状态标签不得使用实心白字样式') + } +}) + +test('附件、审批时间轴与通用触达尺寸满足移动端契约', async () => { + const [attachments, timeline, styles] = await Promise.all([ + read('src/components/mp/MpAttachmentGrid.vue'), + read('src/components/mp/MpApprovalTimeline.vue'), + read('src/styles/mp-crm-primitives.scss') + ]) + assert.match(attachments, /class="att-grid\b/) + assert.match(attachments, /class="att-img"/) + assert.match(attachments, /class="att-doc-thumb"/) + assert.match(attachments, /class="att-add mp-wv-touch-target"/) + assert.match(timeline, /class="mp-crm-approval-timeline"/) + assert.match(timeline, /class="mp-crm-approval-timeline__avatar"/) + assert.match( + styles, + /\.mp-crm-att-grid \.att-(?:item|img|doc-thumb|add)[^{]*\{[\s\S]*?width:\s*72px;[\s\S]*?height:\s*72px;/ + ) + assert.match( + styles, + /\.mp-wv-touch-target\s*\{[\s\S]*?min-width:\s*44px;[\s\S]*?min-height:\s*44px;/ + ) +}) diff --git a/web-admin/src/components/mp/MpBusinessHeader.vue b/web-admin/src/components/mp/MpBusinessHeader.vue new file mode 100644 index 000000000..ebe8ff984 --- /dev/null +++ b/web-admin/src/components/mp/MpBusinessHeader.vue @@ -0,0 +1,61 @@ + + + diff --git a/web-admin/src/styles/mp-webview.scss b/web-admin/src/styles/mp-webview.scss index 1ba53f1a6..66a23a850 100644 --- a/web-admin/src/styles/mp-webview.scss +++ b/web-admin/src/styles/mp-webview.scss @@ -54,6 +54,15 @@ .mp-wv-header__row .mp-wv-header__title { flex: 1; + padding-right: 4px; +} + +.mp-wv-header__actions { + display: flex; + flex-shrink: 0; + gap: 8px; + align-items: center; + margin-left: auto; } .mp-wv-header__row .el-tag { diff --git a/web-admin/src/views/m/crm/product/categories.vue b/web-admin/src/views/m/crm/product/categories.vue index cfd26dcc7..55a73978f 100644 --- a/web-admin/src/views/m/crm/product/categories.vue +++ b/web-admin/src/views/m/crm/product/categories.vue @@ -1,18 +1,19 @@