完成50%

This commit is contained in:
2026-07-23 08:23:59 +08:00
parent 9b0ad62df4
commit f1edc6b533
218 changed files with 24318 additions and 5514 deletions
@@ -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(')) {