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 <cursoragent@cursor.com>
This commit is contained in:
1 parent
efe7f77a66
commit
ef71e842ee
4 files changed
+176
-18
No files matched your search
@@ -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, /<nav[^>]*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, /<article[\s\S]*?role="link"/, '卡片容器不得形成嵌套交互语义')
|
||||
assert.match(card, /class="expense-card__open mp-wv-touch-target"/)
|
||||
assert.match(styles, /\.status-tag--success[^{]*\{[\s\S]*?background:\s*#e9f9f2;/)
|
||||
const statusBlocks = [...styles.matchAll(/\.status-tag(?:--\w+)?[^{]*\{([^}]*)\}/g)]
|
||||
assert.ok(statusBlocks.length >= 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;/
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<header class="mp-wv-header">
|
||||
<div class="mp-wv-header__row">
|
||||
<button
|
||||
class="mp-wv-header__back mp-wv-touch-target"
|
||||
type="button"
|
||||
:aria-label="backLabel"
|
||||
@click="emit('back')"
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<h1 class="mp-wv-header__title">{{ title }}</h1>
|
||||
<div v-if="$slots.actions" class="mp-wv-header__actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
<slot name="before-search" />
|
||||
<input
|
||||
v-if="searchable"
|
||||
class="mp-wv-search mp-wv-touch-target"
|
||||
type="search"
|
||||
:value="keyword"
|
||||
:aria-label="searchLabel"
|
||||
:placeholder="searchPlaceholder"
|
||||
autocomplete="off"
|
||||
@input="updateKeyword"
|
||||
@keyup.enter="emit('search')"
|
||||
/>
|
||||
<slot name="after-search" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
backLabel?: string
|
||||
searchable?: boolean
|
||||
keyword?: string
|
||||
searchPlaceholder?: string
|
||||
searchLabel?: string
|
||||
}>(),
|
||||
{
|
||||
backLabel: '返回',
|
||||
searchable: false,
|
||||
keyword: '',
|
||||
searchPlaceholder: '搜索',
|
||||
searchLabel: '搜索'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
back: []
|
||||
search: []
|
||||
'update:keyword': [value: string]
|
||||
}>()
|
||||
|
||||
function updateKeyword(event: Event) {
|
||||
emit('update:keyword', (event.target as HTMLInputElement).value)
|
||||
}
|
||||
</script>
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<MpWebviewShell @ready="onShellReady">
|
||||
<div class="mp-product-page">
|
||||
<MpBusinessHeader title="产品分类" back-label="返回产品列表" @back="goBack" />
|
||||
<div class="mp-cat-header-tools">
|
||||
<MpBusinessHeader title="产品分类" back-label="返回产品列表" @back="goBack">
|
||||
<template #actions>
|
||||
<button
|
||||
v-if="flatNodes.length"
|
||||
v-if="hasExpandableNodes"
|
||||
class="mp-cat-header-btn"
|
||||
type="button"
|
||||
@click="toggleExpandAll"
|
||||
@@ -12,7 +12,8 @@
|
||||
{{ expandAllLabel }}
|
||||
</button>
|
||||
<button class="mp-cat-header-btn" type="button" @click="openAdd()">新增</button>
|
||||
</div>
|
||||
</template>
|
||||
</MpBusinessHeader>
|
||||
|
||||
<div v-if="loading && !tree.length" class="mp-product-loading">
|
||||
<van-loading vertical>正在加载分类</van-loading>
|
||||
@@ -284,6 +285,10 @@
|
||||
return 'success'
|
||||
}
|
||||
|
||||
const hasExpandableNodes = computed(
|
||||
() => Object.keys(collectExpandIds(tree.value)).length > 0
|
||||
)
|
||||
|
||||
const expandAllLabel = computed(() =>
|
||||
areAllExpandableExpanded(tree.value, expandedIds.value) ? '全部折叠' : '全部展开'
|
||||
)
|
||||
@@ -461,20 +466,6 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mp-product-page {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mp-cat-header-tools {
|
||||
position: absolute;
|
||||
top: calc(8px + env(safe-area-inset-top, 0px));
|
||||
right: 12px;
|
||||
z-index: 60;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mp-cat-header-btn {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
|
||||
Reference in new issue
Block a user