完成50%
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
function Read-Utf8([string]$path) { Get-Content -LiteralPath (Join-Path $root $path) -Raw -Encoding UTF8 }
|
||||
|
||||
@@ -12,24 +12,44 @@ $header = Read-Utf8 'components/PageHeader.vue'
|
||||
foreach ($pattern in @(
|
||||
'(?s)<button[^>]+class="header-back"[^>]+aria-label=',
|
||||
'(?s)<button[^>]+class="header-icon-button header-notice"[^>]+aria-label=',
|
||||
'(?s)<button[^>]+class="header-icon-button"[^>]+aria-label='
|
||||
'(?s)<view\s+v-if="root"\s+class="header-brand"\s+aria-hidden="true"\s*>',
|
||||
'(?s)v-if="root && notice"[^>]+:aria-label="unreadCount > 0',
|
||||
'(?s)v-else-if="root && action"[^>]+class="header-action header-action--root"',
|
||||
'(?s)v-else-if="action"\s+class="header-action"',
|
||||
'(?s)<view\s+v-else\s+class="header-action-placeholder"\s+aria-hidden="true"\s*></view>'
|
||||
)) {
|
||||
if ($header -notmatch $pattern) { throw "PageHeader accessibility contract missing: $pattern" }
|
||||
}
|
||||
if (-not $header.Contains('fallbackUrl')) { throw 'PageHeader must expose a deep-link fallback route' }
|
||||
if ($header.Contains('fallbackUrl')) { throw 'PageHeader must not expose a duplicate deep-link fallback route' }
|
||||
foreach ($token in @(
|
||||
'import { goBack } from "@/utils/navigation.js";',
|
||||
'customBack: { type: Boolean, default: false }',
|
||||
'const emit = defineEmits(["brand", "notice", "action", "back"]);',
|
||||
'const emit = defineEmits(["notice", "action", "back"]);',
|
||||
'@click="handleBack"',
|
||||
'if (props.customBack) {',
|
||||
'emit("back");',
|
||||
'uni.navigateBack();',
|
||||
'uni.reLaunch({ url: props.fallbackUrl });'
|
||||
'return goBack();'
|
||||
)) {
|
||||
if (-not $header.Contains($token)) { throw "PageHeader back contract missing: $token" }
|
||||
}
|
||||
if ($header -match 'aria-label="返回首页"|@click="\$emit\(''brand''\)"') {
|
||||
throw 'PageHeader root brand without an action must be decorative and non-focusable'
|
||||
}
|
||||
if ($header -match ':disabled="!action"') {
|
||||
throw 'PageHeader without an action must render a decorative placeholder instead of an unnamed disabled button'
|
||||
}
|
||||
if ($header -match 'getCurrentPages\s*\(|uni\.(navigateBack|reLaunch)\s*\(') {
|
||||
throw 'PageHeader must delegate all ordinary back semantics to the navigation gateway'
|
||||
}
|
||||
if ($header -notmatch '(?s)const handleBack = \(\) => \{\s*if \(props\.customBack\) \{\s*emit\("back"\);\s*return;\s*\}\s*return goBack\(\);\s*\};') {
|
||||
throw 'PageHeader custom back must stop after emitting; ordinary back must return the gateway Promise'
|
||||
}
|
||||
if ($header -notmatch '(?s)\.header-action\s*\{[^}]*display:\s*flex;[^}]*width:\s*100%;[^}]*min-height:\s*88rpx;[^}]*align-items:\s*center;[^}]*justify-content:\s*flex-end;') {
|
||||
throw 'PageHeader text actions must provide an 88rpx native touch target'
|
||||
}
|
||||
|
||||
$dialog = Read-Utf8 'components/AppDialog.vue'
|
||||
foreach ($token in @('role="dialog"', 'aria-modal="true"', 'tabindex="-1"', '@keydown.esc.stop="cancel"', 'max-height: calc(var(--app-viewport-height, 100vh) - 80rpx - env(safe-area-inset-top) - env(safe-area-inset-bottom))', 'overflow-y: auto')) {
|
||||
foreach ($token in @('role="dialog"', 'aria-modal="true"', 'tabindex="-1"', '@keydown.esc.stop.prevent="cancel"', '@keydown.tab="trapFocus"', 'const trapFocus = (event) =>', 'querySelectorAll(', 'event.preventDefault()', 'max-height: calc(var(--app-viewport-height, 100vh) - 80rpx - env(safe-area-inset-top) - env(safe-area-inset-bottom))', 'overflow-y: auto')) {
|
||||
if (-not $dialog.Contains($token)) { throw "AppDialog accessibility contract missing: $token" }
|
||||
}
|
||||
|
||||
@@ -44,9 +64,48 @@ foreach ($token in @('role="status"', 'aria-live="polite"', 'aria-busy="true"',
|
||||
}
|
||||
|
||||
$tabbar = Read-Utf8 'components/AppTabbar.vue'
|
||||
foreach ($token in @('role="tablist"', 'role="tab"', ':aria-selected="active === item.key"', 'hover-class="tab-item--pressed"')) {
|
||||
foreach ($token in @(
|
||||
'<button',
|
||||
'role="tablist"',
|
||||
'role="tab"',
|
||||
':aria-selected="active === item.key"',
|
||||
'hover-class="tab-item--pressed"',
|
||||
'aria-hidden="true"',
|
||||
'import { goRoot } from "@/utils/navigation.js";',
|
||||
'if (item.key === props.active) return;',
|
||||
'return goRoot(item.routeKey);'
|
||||
)) {
|
||||
if (-not $tabbar.Contains($token)) { throw "AppTabbar interaction contract missing: $token" }
|
||||
}
|
||||
if ($tabbar -match '(?s)<view\s+v-for="item in items"[^>]+role="tab"') {
|
||||
throw 'AppTabbar interactive tabs must use native buttons for keyboard activation'
|
||||
}
|
||||
if ($tabbar -notmatch '(?s)\.tab-item\s*\{[^}]*margin:\s*0;[^}]*border:\s*0;[^}]*border-radius:\s*0;[^}]*background:\s*transparent;[^}]*line-height:\s*normal;' -or $tabbar -notmatch '(?s)\.tab-item::after\s*\{[^}]*border:\s*0;') {
|
||||
throw 'AppTabbar native buttons must reset platform default geometry and decoration'
|
||||
}
|
||||
foreach ($mapping in @(
|
||||
@{ Key = 'genealogy'; RouteKey = 'G01' },
|
||||
@{ Key = 'family'; RouteKey = 'F01' },
|
||||
@{ Key = 'profile'; RouteKey = 'M01' }
|
||||
)) {
|
||||
$pattern = '(?s)\{\s*key:\s*"' + $mapping.Key + '",\s*label:\s*"[^"]+",\s*routeKey:\s*"' + $mapping.RouteKey + '",'
|
||||
if ($tabbar -notmatch $pattern) {
|
||||
throw "AppTabbar mapping drift: $($mapping.Key) -> $($mapping.RouteKey)"
|
||||
}
|
||||
}
|
||||
$itemsBlock = [regex]::Match($tabbar, '(?s)const items = \[(?<Body>.*?)\];')
|
||||
if (-not $itemsBlock.Success) { throw 'AppTabbar must declare its owned root item list' }
|
||||
$itemCount = ([regex]::Matches($itemsBlock.Groups['Body'].Value, '\bkey:\s*"[^"]+"')).Count
|
||||
$approvedItemCount = ([regex]::Matches($itemsBlock.Groups['Body'].Value, '\bkey:\s*"(?:genealogy|family|profile)"')).Count
|
||||
if ($itemCount -ne 3 -or $approvedItemCount -ne 3) {
|
||||
throw "AppTabbar must own exactly the three approved root items, found $itemCount total / $approvedItemCount approved"
|
||||
}
|
||||
if ($tabbar -match '\bpath\s*:|/pages/|uni\.reLaunch\s*\(') {
|
||||
throw 'AppTabbar must consume root route keys instead of paths or direct Uni navigation'
|
||||
}
|
||||
if ($tabbar -notmatch '(?s)const switchRoot = \(item\) => \{\s*if \(item\.key === props\.active\) return;\s*return goRoot\(item\.routeKey\);\s*\};') {
|
||||
throw 'AppTabbar active items must be no-ops and inactive items must return the gateway Promise'
|
||||
}
|
||||
|
||||
$card = Read-Utf8 'components/GenealogyCard.vue'
|
||||
foreach ($token in @('role="button"', ':aria-label="accessibleLabel"', 'hover-class="genealogy-card--pressed"', 'const accessibleLabel = computed(')) {
|
||||
|
||||
Reference in New Issue
Block a user